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
2026-08-06 14:16:40 +09:00
parent b07d6c8953
commit 6c8381f164
155 changed files with 17192 additions and 1829 deletions
+1
View File
@@ -189,6 +189,7 @@
#### 안내 문구·주소 처리
- 일부 안내가 실제 문장 대신 내부 식별자(`messages.auth.unauthenticated` 같은 문자열)로 표시되던 문제를 수정했습니다. 로그인하지 않았거나 역할 권한이 없어 요청이 거절될 때, 메뉴에 역할을 지정할 때, 확장 식별자를 잘못 입력했을 때, 활동 로그를 기간·유형으로 검색할 때가 해당됩니다.
- 회원 정보 수정, 권한·메뉴 설정, 본인인증, 알림 설정 등 여러 화면에서 처리를 마친 뒤 표시되는 안내 문구가 번역된 문장 대신 내부 식별자가 그대로 보이던 문제를 수정했습니다. 이제 성공·실패·권한 없음 등 모든 안내가 사용 중인 언어에 맞는 문장으로 표시됩니다.
- 환경설정 저장이 실패했을 때 안내 문구에 항목 이름이 `security.password min length` 같은 내부 식별자로 표시되던 문제를 수정했습니다. 보안·업로드·SEO·고급·드라이버·메일 등 모든 설정 항목이 화면에 보이는 한글 항목명으로 안내됩니다.
- 플러그인을 GitHub 주소나 ZIP 파일로 설치할 때의 입력 안내가 내부 식별자로 표시되던 문제를 수정했습니다. 주소 형식이 잘못됐거나, ZIP이 아닌 파일을 올렸거나, 파일 용량을 초과했을 때 정상 문장으로 안내합니다.
- 목록의 열 제목을 눌러 정렬할 때, 이미 다른 열로 정렬 중이었다면 화살표는 새로 누른 열에 표시되면서 실제 정렬은 이전 열 기준으로 바뀌던 문제를 수정했습니다. 표시와 실제 정렬 기준이 어긋나 결과를 신뢰할 수 없었으며, 이제 누른 열 기준으로 한 번에 정렬됩니다.
+133 -4
View File
@@ -126,6 +126,7 @@ class ApiDocgenCommand extends Command
$stats = ['files' => 0, 'endpoints' => 0, 'probed' => 0, 'skipped' => 0, 'examples' => 0];
$checkFindings = [];
$skippedProbes = [];
$examplesOnly = (bool) $this->option('examples-only');
// 대상(코어/확장)별 README 목차 항목 누적: readmeKey => ['label' => ..., 'entries' => [...]]
@@ -136,6 +137,17 @@ class ApiDocgenCommand extends Command
$sectionKeys = [];
$exampleBlocks = [];
// `Route::match(['get','post'], ...)->name('x')` 는 하나의 라우트명으로 두 엔드포인트를
// 등록한다. 생성 블록 키가 라우트명뿐이면 한 문서에 같은 키가 둘 생겨, 블록 조회(strpos)가
// 언제나 첫 블록만 잡아 두 번째 블록의 사람 서술이 재생성마다 유실된다.
// 문서 안에서 중복되는 라우트명만 골라 키에 메서드를 붙인다 (나머지 키는 그대로 — 전 문서의
// 키를 바꾸면 기존 블록과 대조가 어긋나 사람 서술이 통째로 사라진다).
$nameCounts = array_count_values(array_map(
static fn (array $r): string => $r['name'] ?: $r['uri'],
$items,
));
$duplicatedNames = array_keys(array_filter($nameCounts, static fn (int $n): bool => $n > 1));
// 목차용 도메인 파일 항목 (소유별). 예시-only/check 모드에서도 목차는 최신화한다.
$owner = $items[0]['owner'];
$readmeKey = $this->readmeFile($owner);
@@ -156,10 +168,14 @@ class ApiDocgenCommand extends Command
$stats['probed']++;
} else {
$stats['skipped']++;
$checkFindings[] = "{$route['method']} {$route['uri']} — {$probeMeta['skipped_reason']}";
// 실측 제외는 설계상 정상이다 — 부수효과 쓰기·미치환 path·검증 실패는 의도적으로
// 호출하지 않고, 그 자리는 사람이 코드 근거로 채운다. drift 로 세면 문서가 완전해도
// --check 가 통과할 수 없어 기준이 무의미해진다. 정보로만 남긴다.
$skippedProbes[] = "{$route['method']} {$route['uri']} — {$probeMeta['skipped_reason']}";
}
$key = $route['name'] ?: $route['uri'];
$methodScopedKey = in_array($route['name'] ?: $route['uri'], $duplicatedNames, true);
$key = $scaffolder->generatedKey($route, $methodScopedKey);
if ($examplesOnly) {
// 표·서술을 건드리지 않고 예시 2블록만 산출 (SSoT=스캐폴더 예시 메서드).
@@ -179,7 +195,7 @@ class ApiDocgenCommand extends Command
}
$commentMap = $this->columnComments($route, $commentResolver, $sampleMap);
$sections[] = $scaffolder->endpointSection($route, $request, $schema, $probeMeta, $commentMap);
$sections[] = $scaffolder->endpointSection($route, $request, $schema, $probeMeta, $commentMap, $methodScopedKey);
$sectionKeys[] = $key;
$stats['endpoints']++;
}
@@ -187,6 +203,12 @@ class ApiDocgenCommand extends Command
if ($this->option('check')) {
if (! File::exists($file)) {
$checkFindings[] = "문서 파일 없음: {$file}";
} else {
// 실측 불가 자리를 사람이 채우지 않고 마커로 남겨 두면 문서가 미완이다.
// (규정: docs/backend/api-documentation.md "미채움 마커 5종")
foreach ($this->unfilledMarkers(File::get($file)) as $marker) {
$checkFindings[] = "미채움 마커: {$file} — {$marker}";
}
}
continue;
@@ -216,6 +238,15 @@ class ApiDocgenCommand extends Command
$header = $this->documentHeader($file, $items[0]);
$existing = File::exists($file) ? File::get($file) : null;
// 기존 문서는 중복 라우트명 블록을 구 키(라우트명만)로 갖고 있다. 그대로 두면 새 키
// (라우트명::메서드)와 대조되지 않아 이번 재생성에서 그 블록의 사람 서술이 유실된다.
// 병합 전에 구 키를 등장 순서대로 새 키로 승격한다 — 문서 내 블록 순서는 $items 순서와
// 같으므로(같은 그룹을 같은 순서로 순회) 메서드 대응이 어긋나지 않는다.
if ($existing !== null && $duplicatedNames !== []) {
$existing = $this->migrateDuplicateBlockKeys($existing, $items, $duplicatedNames, $scaffolder);
}
$content = $scaffolder->mergeDocument($existing, $header, $sections, $sectionKeys);
File::ensureDirectoryExists(dirname($file));
@@ -253,8 +284,20 @@ class ApiDocgenCommand extends Command
}
if ($this->option('check')) {
// 실측 제외는 정상 동작이므로 참고 정보로만 표시한다 (drift 아님).
if ($skippedProbes !== []) {
$this->line('실측 제외 '.count($skippedProbes).'건 (정상 — 해당 자리는 사람이 작성):');
foreach (array_slice($skippedProbes, 0, 10) as $s) {
$this->line(' · '.$s);
}
if (count($skippedProbes) > 10) {
$this->line(' · ... 외 '.(count($skippedProbes) - 10).'건');
}
$this->newLine();
}
if ($checkFindings !== []) {
$this->warn('실측 제외/문서 누락 '.count($checkFindings).'건:');
$this->warn('drift '.count($checkFindings).'건:');
foreach (array_slice($checkFindings, 0, 50) as $f) {
$this->line(' - '.$f);
}
@@ -899,6 +942,92 @@ class ApiDocgenCommand extends Command
return base_path("{$base}/_bundled/{$owner['id']}/docs/api/README.md");
}
/**
* 문서에 남아 있는 미채움 마커를 수집합니다.
*
* `api:docgen` 은 실측 불가한 자리에 "사람이 작성하세요" 마커를 남긴다. 실측 불가는 그 자리를
* 비워둘 사유가 아니며, 컨트롤러/Resource/FormRequest/Enum/lang 을 읽어 채우는 것이 규정이다
* (docs/backend/api-documentation.md "미채움 마커 5종"). 마커가 남아 있으면 문서는 미완이다.
*
* 잔량 전수 집계는 `check-api-doc-unfilled.cjs` 가 담당하고, 여기서는 `--check` 가 자기 scope
* 문서의 미완을 drift 로 잡기 위해 마커 종류만 요약한다.
*
* @param string $content 문서 내용
* @return array<int, string> 발견된 마커 요약 (종류 => 건수)
*/
private function unfilledMarkers(string $content): array
{
$markers = [
'실측 제외:' => '실측 제외 (응답 필드 표 / 응답 예시)',
'TODO: 용도' => '요청 파라미터 설명',
'TODO: 설명' => '응답 필드 설명',
'실측 응답에 필드 없음' => '빈 목록 관측 (응답 필드 표)',
'대표 에러 없음' => '도메인 특이 에러',
];
$found = [];
foreach ($markers as $needle => $label) {
$count = substr_count($content, $needle);
if ($count > 0) {
$found[] = "{$label} {$count}건";
}
}
return $found;
}
/**
* 기존 문서의 중복 생성 블록 키를 메서드 스코프 키로 승격합니다.
*
* `Route::match(['get','post'], ...)->name('x')` 로 등록된 라우트는 한 문서에 같은
* `@generated:start:x` 블록을 둘 만든다. 새 키(`x::get` / `x::post`)로 재생성하면 구 키를
* 가진 기존 블록과 대조되지 않아 그 블록의 사람 서술이 유실되므로, 병합 전에 구 키를
* 등장 순서대로 새 키로 바꿔 준다.
*
* 문서 내 블록 순서는 `$items` 순서와 같다(같은 그룹을 같은 순서로 순회해 생성했다).
* 따라서 n 번째 구 키 블록에는 그 라우트명을 가진 n 번째 라우트의 메서드를 붙인다.
*
* @param string $existing 기존 문서 내용
* @param array<int, array<string, mixed>> $items 이 문서의 라우트 목록 (문서 순서)
* @param array<int, string> $duplicatedNames 문서 안에서 중복되는 라우트명
* @param ApiDocScaffolder $scaffolder 키 생성기
* @return string 키가 승격된 문서 내용
*/
private function migrateDuplicateBlockKeys(
string $existing,
array $items,
array $duplicatedNames,
ApiDocScaffolder $scaffolder
): string {
foreach ($duplicatedNames as $name) {
$methods = array_values(array_map(
static fn (array $r): string => $r['method'],
array_filter($items, static fn (array $r): bool => ($r['name'] ?: $r['uri']) === $name),
));
$marker = '<!-- @generated:start:'.$name.' -->';
$offset = 0;
foreach ($methods as $method) {
$pos = strpos($existing, $marker, $offset);
if ($pos === false) {
break;
}
$newKey = $scaffolder->generatedKey(['name' => $name, 'uri' => $name, 'method' => $method], true);
$newMarker = '<!-- @generated:start:'.$newKey.' -->';
$existing = substr_replace($existing, $newMarker, $pos, strlen($marker));
$offset = $pos + strlen($newMarker);
}
}
return $existing;
}
/**
* 문서 헤더(제목 + TL;DR)를 생성합니다.
*
+18 -18
View File
@@ -21,7 +21,7 @@ class ResponseHelper
/**
* 성공 응답을 생성합니다.
*
* @param string $messageKey 메시지 키 (기본값: 'messages.success')
* @param string $messageKey 메시지 키 (기본값: 'common.success')
* @param mixed $data 응답 데이터
* @param int $statusCode HTTP 상태 코드 (기본값: 200)
* @param array $messageParams 메시지 매개변수
@@ -29,7 +29,7 @@ class ResponseHelper
* @return JsonResponse JSON 응답
*/
public static function success(
string $messageKey = 'messages.success',
string $messageKey = 'common.success',
mixed $data = null,
int $statusCode = 200,
array $messageParams = [],
@@ -45,7 +45,7 @@ class ResponseHelper
/**
* 실패 응답을 생성합니다.
*
* @param string $messageKey 메시지 키 (기본값: 'messages.failed')
* @param string $messageKey 메시지 키 (기본값: 'common.failed')
* @param int $statusCode HTTP 상태 코드 (기본값: 400)
* @param mixed $errors 오류 정보
* @param array $messageParams 메시지 매개변수
@@ -53,7 +53,7 @@ class ResponseHelper
* @return JsonResponse JSON 응답
*/
public static function error(
string $messageKey = 'messages.failed',
string $messageKey = 'common.failed',
int $statusCode = 400,
mixed $errors = null,
array $messageParams = [],
@@ -87,14 +87,14 @@ class ResponseHelper
* 입력 검증 실패 응답을 생성합니다.
*
* @param mixed $errors 검증 오류 정보
* @param string $messageKey 메시지 키 (기본값: 'messages.validation_failed')
* @param string $messageKey 메시지 키 (기본값: 'common.validation_failed')
* @param array $messageParams 메시지 매개변수
* @param string $domain 번역 도메인 (기본값: 'core')
* @return JsonResponse 422 상태 코드를 가진 JSON 응답
*/
public static function validationError(
mixed $errors,
string $messageKey = 'messages.validation_failed',
string $messageKey = 'common.validation_failed',
array $messageParams = [],
string $domain = 'core'
): JsonResponse {
@@ -108,13 +108,13 @@ class ResponseHelper
/**
* 인증 실패 응답을 생성합니다.
*
* @param string $messageKey 메시지 키 (기본값: 'messages.unauthorized')
* @param string $messageKey 메시지 키 (기본값: 'common.unauthorized')
* @param array $messageParams 메시지 매개변수
* @param string $domain 번역 도메인 (기본값: 'core')
* @return JsonResponse 401 상태 코드를 가진 JSON 응답
*/
public static function unauthorized(
string $messageKey = 'messages.unauthorized',
string $messageKey = 'common.unauthorized',
array $messageParams = [],
string $domain = 'core'
): JsonResponse {
@@ -127,13 +127,13 @@ class ResponseHelper
/**
* 권한 부족 응답을 생성합니다.
*
* @param string $messageKey 멤시지 키 (기본값: 'messages.forbidden')
* @param string $messageKey 멤시지 키 (기본값: 'common.forbidden')
* @param array $messageParams 멤시지 매개변수
* @param string $domain 번역 도메인 (기본값: 'core')
* @return JsonResponse 403 상태 코드를 가진 JSON 응답
*/
public static function forbidden(
string $messageKey = 'messages.forbidden',
string $messageKey = 'common.forbidden',
array $messageParams = [],
string $domain = 'core'
): JsonResponse {
@@ -146,13 +146,13 @@ class ResponseHelper
/**
* 리소스를 찾을 수 없음 응답을 생성합니다.
*
* @param string $messageKey 멤시지 키 (기본값: 'messages.not_found')
* @param string $messageKey 멤시지 키 (기본값: 'common.not_found')
* @param array $messageParams 멤시지 매개변수
* @param string $domain 번역 도메인 (기본값: 'core')
* @return JsonResponse 404 상태 코드를 가진 JSON 응답
*/
public static function notFound(
string $messageKey = 'messages.not_found',
string $messageKey = 'common.not_found',
array $messageParams = [],
string $domain = 'core'
): JsonResponse {
@@ -165,14 +165,14 @@ class ResponseHelper
/**
* 서버 내부 오류 응답을 생성합니다.
*
* @param string $messageKey 멤시지 키 (기본값: 'messages.error_occurred')
* @param string $messageKey 멤시지 키 (기본값: 'common.error_occurred')
* @param mixed $error 오류 정보 (디버그 모드에서만 표시)
* @param array $messageParams 멤시지 매개변수
* @param string $domain 번역 도메인 (기본값: 'core')
* @return JsonResponse 500 상태 코드를 가진 JSON 응답
*/
public static function serverError(
string $messageKey = 'messages.error_occurred',
string $messageKey = 'common.error_occurred',
mixed $error = null,
array $messageParams = [],
string $domain = 'core'
@@ -341,7 +341,7 @@ class ResponseHelper
* (색인 누락 안내)가 사라지면 검색이 0건을 돌려주는 사실을 운영자가 알 방법이 없습니다.
* `success`/`message`/`data` 키는 부가 데이터로 덮어쓰지 않습니다.
*
* @param string $messageKey 멤시지 키 (기본값: 'messages.success')
* @param string $messageKey 메시지 키 (기본값: 'common.success')
* @param JsonResource|ResourceCollection|null $resource JSON 리소스
* @param int $statusCode HTTP 상태 코드 (기본값: 200)
* @param array $messageParams 멤시지 매개변수
@@ -349,7 +349,7 @@ class ResponseHelper
* @return JsonResponse JSON 응답
*/
public static function successWithResource(
string $messageKey = 'messages.success',
string $messageKey = 'common.success',
JsonResource|ResourceCollection|null $resource = null,
int $statusCode = 200,
array $messageParams = [],
@@ -415,14 +415,14 @@ class ResponseHelper
/**
* 페이지네이션된 리소스 응답을 생성합니다.
*
* @param string $messageKey 멤시지 키 (기본값: 'messages.success')
* @param string $messageKey 메시지 키 (기본값: 'common.success')
* @param ResourceCollection|null $collection 페이지네이션된 리소스 컬렉션
* @param array $messageParams 멤시지 매개변수
* @param string $domain 번역 도메인 (기본값: 'core')
* @return JsonResponse 페이지네이션 메타 데이터를 포함한 JSON 응답
*/
public static function successWithPagination(
string $messageKey = 'messages.success',
string $messageKey = 'common.success',
?ResourceCollection $collection = null,
array $messageParams = [],
string $domain = 'core'
@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\Admin\Identity;
use App\Enums\PermissionType;
use App\Http\Controllers\Api\Base\AdminBaseController;
use App\Http\Requests\Identity\AdminIdentityLogIndexRequest;
use App\Http\Requests\Identity\AdminIdentityLogPurgeRequest;
@@ -45,7 +46,7 @@ class AdminIdentityLogController extends AdminBaseController
$paginated = $this->logService->search($filters, (int) ($validated['per_page'] ?? 20));
return $this->success('messages.success', [
return $this->success('common.success', [
'data' => IdentityLogResource::collection(collect($paginated->items()))->resolve(),
'pagination' => [
'current_page' => $paginated->currentPage(),
@@ -59,7 +60,7 @@ class AdminIdentityLogController extends AdminBaseController
'abilities' => [
'can_purge' => $request->user()?->hasPermission(
'core.admin.identity.logs.purge',
\App\Enums\PermissionType::Admin,
PermissionType::Admin,
) ?? false,
],
]);
@@ -76,7 +77,7 @@ class AdminIdentityLogController extends AdminBaseController
$days = (int) ($request->validated()['older_than_days'] ?? 180);
$count = $this->logService->purge($days);
return $this->success('messages.success', [
return $this->success('common.success', [
'purged_count' => $count,
'older_than_days' => $days,
]);
@@ -60,7 +60,7 @@ class AdminIdentityPolicyController extends AdminBaseController
$paginated = $this->policyService->search($filters, $perPage);
$collection = (new PolicyCollection($paginated))->toArray($request);
return $this->success('messages.success', [
return $this->success('common.success', [
'data' => $collection['data'],
'abilities' => $collection['abilities'] ?? [],
'meta' => [
@@ -83,7 +83,7 @@ class AdminIdentityPolicyController extends AdminBaseController
$policy = $this->policyService->createAdminPolicy($request->validated());
return $this->success(
'messages.created',
'common.created',
(new PolicyResource($policy))->toArray($request),
201,
);
@@ -101,7 +101,7 @@ class AdminIdentityPolicyController extends AdminBaseController
$policy = $this->policyService->findById($id);
if (! $policy) {
return $this->error('messages.not_found', 404);
return $this->error('common.not_found', 404);
}
$validated = $request->validated();
@@ -119,13 +119,13 @@ class AdminIdentityPolicyController extends AdminBaseController
}
if (! $this->policyService->updatePolicy($policy, $validated)) {
return $this->error('messages.failed', 500);
return $this->error('common.failed', 500);
}
$policy->refresh();
return $this->success(
'messages.updated',
'common.updated',
(new PolicyResource($policy))->toArray($request),
);
}
@@ -142,16 +142,16 @@ class AdminIdentityPolicyController extends AdminBaseController
$policy = $this->policyService->findById($id);
if (! $policy) {
return $this->error('messages.not_found', 404);
return $this->error('common.not_found', 404);
}
if ($policy->source_type !== IdentityPolicySourceType::Admin) {
return $this->forbidden('messages.cannot_delete_system_resource');
return $this->forbidden('identity.errors.cannot_delete_system_policy');
}
return $this->policyService->deleteAdminPolicy($policy)
? $this->success('messages.deleted')
: $this->error('messages.failed', 500);
? $this->success('common.deleted')
: $this->error('common.failed', 500);
}
/**
@@ -170,7 +170,7 @@ class AdminIdentityPolicyController extends AdminBaseController
$policy = $this->policyService->findById($id);
if (! $policy) {
return $this->error('messages.not_found', 404);
return $this->error('common.not_found', 404);
}
if ($policy->source_type === IdentityPolicySourceType::Admin) {
@@ -184,6 +184,6 @@ class AdminIdentityPolicyController extends AdminBaseController
return $this->error('identity.errors.reset_field_failed', 422);
}
return $this->successWithResource('messages.success', new PolicyResource($policy->fresh()));
return $this->successWithResource('common.success', new PolicyResource($policy->fresh()));
}
}
@@ -5,9 +5,9 @@ namespace App\Http\Controllers\Api\Admin\Identity;
use App\Extension\HookManager;
use App\Extension\IdentityVerification\IdentityVerificationManager;
use App\Http\Controllers\Api\Base\AdminBaseController;
use App\Http\Requests\Identity\AdminIdentityProviderIndexRequest;
use App\Http\Resources\Identity\ProviderResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* 관리자 — IDV 프로바이더 조회 + 설정 스키마 수집 컨트롤러.
@@ -28,10 +28,10 @@ class AdminIdentityProviderController extends AdminBaseController
/**
* 등록된 프로바이더 목록과 각 프로바이더의 설정 스키마를 반환합니다.
*
* @param Request $request HTTP 요청
* @param AdminIdentityProviderIndexRequest $request 검증된 요청 (파라미터 없음)
* @return JsonResponse 프로바이더 목록 (settings_schema 포함)
*/
public function index(Request $request): JsonResponse
public function index(AdminIdentityProviderIndexRequest $request): JsonResponse
{
$providers = array_values($this->manager->all());
@@ -48,6 +48,6 @@ class AdminIdentityProviderController extends AdminBaseController
return $resource + ['settings_schema' => is_array($schema) ? $schema : []];
}, $providers);
return $this->success('messages.success', $rows);
return $this->success('common.success', $rows);
}
}
@@ -63,7 +63,7 @@ class PluginSettingsController extends AdminBaseController
return $this->notFound('plugins.not_found');
}
return $this->success('messages.success', $this->maskSensitive($identifier, $settings));
return $this->success('common.success', $this->maskSensitive($identifier, $settings));
}
/**
@@ -109,6 +109,6 @@ class PluginSettingsController extends AdminBaseController
return $this->notFound('plugins.not_found');
}
return $this->success('messages.success', $layout);
return $this->success('common.success', $layout);
}
}
@@ -48,9 +48,9 @@ class SeoCacheController extends AdminBaseController
'by_module' => $this->statsService->getStatsByModule($since),
];
return $this->success('messages.success', $data);
return $this->success('common.success', $data);
} catch (\Exception $e) {
return $this->error('messages.error_occurred', 500, $e->getMessage());
return $this->error('common.error_occurred', 500, $e->getMessage());
}
}
@@ -70,14 +70,14 @@ class SeoCacheController extends AdminBaseController
if ($layout) {
$count = $this->cacheManager->invalidateByLayout($layout);
return $this->success('messages.success', ['cleared' => $count]);
return $this->success('common.success', ['cleared' => $count]);
}
$this->cacheManager->clearAll();
return $this->success('messages.success', ['cleared' => 'all']);
return $this->success('common.success', ['cleared' => 'all']);
} catch (\Exception $e) {
return $this->error('messages.error_occurred', 500, $e->getMessage());
return $this->error('common.error_occurred', 500, $e->getMessage());
}
}
@@ -92,12 +92,12 @@ class SeoCacheController extends AdminBaseController
{
try {
// Phase 5의 SeoDeclarationCollector 구현 후 실제 워밍업 로직 추가 예정
return $this->success('messages.success', [
return $this->success('common.success', [
'status' => 'dispatched',
'message' => __('seo.warmup_dispatched'),
]);
} catch (\Exception $e) {
return $this->error('messages.error_occurred', 500, $e->getMessage());
return $this->error('common.error_occurred', 500, $e->getMessage());
}
}
@@ -149,9 +149,9 @@ class SeoCacheController extends AdminBaseController
try {
$urls = $this->cacheManager->getCachedUrls();
return $this->success('messages.success', ['urls' => $urls, 'count' => count($urls)]);
return $this->success('common.success', ['urls' => $urls, 'count' => count($urls)]);
} catch (\Exception $e) {
return $this->error('messages.error_occurred', 500, $e->getMessage());
return $this->error('common.error_occurred', 500, $e->getMessage());
}
}
}
@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\Identity;
use App\Enums\IdentityOriginType;
use App\Extension\IdentityVerification\IdentityVerificationManager;
use App\Http\Controllers\Api\Base\PublicBaseController;
use App\Http\Requests\Identity\CancelChallengeRequest;
@@ -15,6 +16,7 @@ use App\Http\Requests\Identity\VerifyChallengeRequest;
use App\Http\Resources\Identity\ChallengeResource;
use App\Http\Resources\Identity\ProviderResource;
use App\Models\IdentityVerificationLog;
use App\Models\User;
use App\Services\IdentityPolicyService;
use App\Services\IdentityVerificationService;
use Illuminate\Http\JsonResponse;
@@ -53,7 +55,7 @@ class IdentityVerificationController extends PublicBaseController
$user = $request->user();
$target = $user ?: ($validated['target'] ?? []);
if (! ($user instanceof \App\Models\User) && empty($target['email']) && empty($target['phone'])) {
if (! ($user instanceof User) && empty($target['email']) && empty($target['phone'])) {
return $this->error('identity.errors.missing_target', 422);
}
@@ -68,7 +70,7 @@ class IdentityVerificationController extends PublicBaseController
context: [
'ip_address' => $request->ip(),
'user_agent' => substr((string) $request->userAgent(), 0, 512),
'origin_type' => \App\Enums\IdentityOriginType::Api->value,
'origin_type' => IdentityOriginType::Api->value,
'origin_identifier' => '/api/identity/challenges',
],
providerId: $providerId,
@@ -136,7 +138,7 @@ class IdentityVerificationController extends PublicBaseController
*
* @param CancelChallengeRequest $request 검증된 요청
* @param IdentityVerificationLog $challenge 라우트 모델 바인딩으로 resolve 된 challenge 로그
* @return JsonResponse
* @return JsonResponse 취소 결과
*/
public function cancel(CancelChallengeRequest $request, IdentityVerificationLog $challenge): JsonResponse
{
@@ -159,7 +161,8 @@ class IdentityVerificationController extends PublicBaseController
*
* @param ShowChallengeRequest $request 검증된 요청
* @param IdentityVerificationLog $challenge 라우트 모델 바인딩으로 resolve 된 challenge 로그
* @return JsonResponse
* @return JsonResponse 공개 안전 상태 필드
*
* @since engine-v1.46.0
*/
public function show(ShowChallengeRequest $request, IdentityVerificationLog $challenge): JsonResponse
@@ -170,7 +173,7 @@ class IdentityVerificationController extends PublicBaseController
return $this->error('identity.errors.challenge_not_found', 404);
}
return $this->success('messages.success', $status);
return $this->success('common.success', $status);
}
/**
@@ -188,6 +191,7 @@ class IdentityVerificationController extends PublicBaseController
* @param IdentityCallbackRequest $request 검증된 요청
* @param string $providerId 콜백을 보낸 provider 식별자
* @return JsonResponse|RedirectResponse
*
* @since engine-v1.46.0
*/
public function callback(IdentityCallbackRequest $request, string $providerId)
@@ -271,7 +275,7 @@ class IdentityVerificationController extends PublicBaseController
$providers,
);
return $this->success('messages.success', $data);
return $this->success('common.success', $data);
}
/**
@@ -300,7 +304,7 @@ class IdentityVerificationController extends PublicBaseController
];
}
return $this->success('messages.success', $data);
return $this->success('common.success', $data);
}
/**
@@ -353,11 +357,11 @@ class IdentityVerificationController extends PublicBaseController
$policy = $this->policyService->resolve($scope, $target);
if (! $policy || ! $policy->enabled) {
return $this->success('messages.success', null);
return $this->success('common.success', null);
}
// 민감 필드는 노출하지 않고 UI 힌트에 필요한 최소 필드만 반환
return $this->success('messages.success', [
return $this->success('common.success', [
'policy_key' => $policy->key,
'scope' => $policy->scope,
'target' => $policy->target,
@@ -0,0 +1,38 @@
<?php
namespace App\Http\Requests\Identity;
use Illuminate\Foundation\Http\FormRequest;
/**
* 관리자 IDV 프로바이더 목록 조회 검증.
*
* 등록된 프로바이더 전체를 설정 스키마와 함께 반환하는 엔드포인트로, 필터/페이지네이션
* 파라미터를 받지 않습니다. 검증 규칙은 비어 있지만, 컨트롤러가 base `Illuminate\Http\Request`
* 를 주입받지 않도록 전용 FormRequest 를 둡니다 (규정: docs/backend/validation.md).
*
* 공개용 `ProvidersIndexRequest`(인증 없는 메타데이터 조회)와 달리 이쪽은 관리자 전용이며,
* 인증/권한은 라우트의 permission 미들웨어가 담당합니다.
*/
class AdminIdentityProviderIndexRequest extends FormRequest
{
/**
* 요청 권한 — 라우트 permission 미들웨어가 담당하므로 true 고정.
*
* @return bool 항상 true (권한은 미들웨어 체인에서 처리)
*/
public function authorize(): bool
{
return true;
}
/**
* 검증 규칙을 반환합니다.
*
* @return array<string, array<int, mixed>> 검증 규칙 (요청 파라미터 없음)
*/
public function rules(): array
{
return [];
}
}
@@ -199,6 +199,9 @@ class IdentityVerificationService
$publicPayload = $log->properties['public_payload'];
}
// 시도 횟수(attempts / max_attempts) 는 노출하지 않는다.
// 이 엔드포인트는 권한 가드 없는 공개 폴링용이라 challenge id 만 알면 누구나 조회할 수 있고,
// 두 값을 함께 알면 남은 시도 횟수를 정확히 계산해 무차별 대입 시점을 노릴 수 있다.
return [
'id' => $log->id,
'status' => $log->status->value,
+98 -10
View File
@@ -69,6 +69,28 @@ class ApiDocScaffolder
private readonly ParameterDescriber $paramDescriber = new ParameterDescriber
) {}
/**
* 엔드포인트의 `@generated` 블록 키를 만듭니다.
*
* 기본은 라우트명이다. 다만 `Route::match(['get','post'], ...)->name('x')` 처럼 **하나의
* 라우트명이 여러 메서드**를 등록하면(KG이니시스 CBT/모바일 콜백), 한 문서에 같은 키의
* 생성 블록이 둘 생긴다. 블록 조회는 `strpos` 로 첫 출현을 잡으므로 두 번째 블록의 사람
* 서술이 재생성마다 유실된다. 그런 라우트에 한해 메서드를 접미로 붙여 키를 고유화한다.
*
* 중복이 아닌 절대다수의 라우트는 키를 그대로 둔다 — 전 문서의 키를 바꾸면 기존 블록과
* 대조가 어긋나 사람 서술이 통째로 유실되기 때문이다.
*
* @param array<string, mixed> $route 라우트 메타데이터
* @param bool $methodScoped 라우트명이 문서 안에서 중복되는가 (메서드 접미 필요)
* @return string 생성 블록 키
*/
public function generatedKey(array $route, bool $methodScoped = false): string
{
$name = $route['name'] ?: $route['uri'];
return $methodScoped ? $name.'::'.strtolower((string) $route['method']) : $name;
}
/**
* 단일 엔드포인트의 마크다운 섹션을 생성합니다.
*
@@ -77,13 +99,14 @@ class ApiDocScaffolder
* @param array<string, mixed>|null $schema 실측 응답 스키마 (null=실측 안 됨)
* @param array<string, mixed> $probeMeta 실측 메타 (status, skipped_reason, base_url, resolved_uri, body)
* @param array<string, string> $commentMap 컬럼명 => 주석 (필드 설명 기본값)
* @param bool $methodScopedKey 라우트명이 문서 안에서 중복되어 메서드 접미가 필요한가
* @return string 마크다운 섹션
*/
public function endpointSection(array $route, array $request, ?array $schema, array $probeMeta, array $commentMap = []): string
public function endpointSection(array $route, array $request, ?array $schema, array $probeMeta, array $commentMap = [], bool $methodScopedKey = false): string
{
$name = $route['name'] ?: '(unnamed)';
$heading = "### {$route['method']} {$route['uri']}";
$genKey = $name;
$genKey = $this->generatedKey($route, $methodScopedKey);
$lines = [];
$lines[] = $heading;
@@ -173,6 +196,9 @@ class ApiDocScaffolder
?string $existing = null,
?array $extensions = null
): string {
// 줄 끝 정규화 — mergeDocument 와 같은 이유 (CRLF 체크아웃에서 줄 단위 매칭이 전부 실패).
$existing = $existing === null ? null : $this->normalizeEol($existing);
usort($entries, fn ($a, $b) => strcmp($a['domain'], $b['domain']));
$total = array_sum(array_column($entries, 'count'));
@@ -472,9 +498,11 @@ class ApiDocScaffolder
}
// 403: admin 게이트 또는 permission 요구 → 권한 부족.
// permission 은 여러 권한의 OR 조합(`core.modules.read | core.menus.read`)일 수 있으므로
// 셀에 박기 전에 파이프를 이스케이프한다 — 안 하면 3열 표가 갈라져 깨진다.
if ($this->hasMiddleware($mw, 'AdminMiddleware') || ! empty($route['permission'])) {
$cond = ! empty($route['permission'])
? "요구 권한(`{$route['permission']}`)이 없는 경우"
? '요구 권한(`'.$this->escapeCell((string) $route['permission']).'`)이 없는 경우'
: '관리자 권한이 없는 경우';
$rows[] = "| 403 | Forbidden | {$cond} |";
}
@@ -1040,6 +1068,24 @@ class ApiDocScaffolder
return str_replace(['|', "\n", "\r"], ['\\|', ' ', ''], $text);
}
/**
* 기존 문서의 줄 끝을 LF 로 정규화합니다.
*
* 보존 로직(표 셀·응답 본문·에러 표·사람 서술)은 전부 `explode("\n")` 후 줄 끝 매칭
* (`str_ends_with($line, ' |')` 등)으로 동작한다. Windows 체크아웃(core.autocrlf=true)이
* 내려준 CRLF 문서에서는 줄 끝에 \r 이 남아 이 매칭이 전부 실패하고, 결과적으로 사람이
* 채운 셀·본문이 하나도 보존되지 않은 채 TODO/마커로 퇴행한다.
*
* 문서는 `.gitattributes` 가 `eol=lf` 로 저장하므로 정규화가 저장 형식을 바꾸지 않는다.
*
* @param string $content 기존 문서 내용
* @return string LF 로 정규화된 내용
*/
private function normalizeEol(string $content): string
{
return str_replace(["\r\n", "\r"], "\n", $content);
}
/**
* 기존 문서에 새 생성 블록을 병합합니다. 사람 서술은 보존합니다.
*
@@ -1055,6 +1101,12 @@ class ApiDocScaffolder
return $header."\n".implode("\n", $sections);
}
// Windows 체크아웃(core.autocrlf=true)은 문서를 CRLF 로 내려준다. 아래 보존 로직은
// 전부 `explode("\n")` + 줄 끝 매칭(`str_ends_with($line, ' |')`) 기반이라, 줄 끝에
// \r 이 남아 있으면 표 행이 하나도 파싱되지 않아 사람이 채운 셀이 통째로 유실된다.
// 파싱 전에 줄 끝을 LF 로 정규화한다 (문서는 .gitattributes 가 eol=lf 로 저장한다).
$existing = $this->normalizeEol($existing);
$merged = $header."\n";
foreach ($sections as $i => $section) {
@@ -1207,17 +1259,30 @@ class ApiDocScaffolder
}
$previousRows = $this->errorTableRows($previous);
if ($previousRows === []) {
return $section;
}
$currentBody = $this->extractErrorTable($section);
if ($currentBody === null) {
return $section;
}
// 기존 본문이 표가 아닌 사람 서술인 경우 (자동 추론이 에러 행을 만들지 못하는 공개 조회
// 엔드포인트에서, 사람이 `대표 에러 없음` 초안 자리를 "왜 도메인 에러가 없는지" 로 채운 것).
// 표 행이 없다고 보존을 건너뛰면 재생성이 그 서술을 TODO 초안으로 덮는다.
if ($previousRows === []) {
$previousBody = $this->extractErrorTable($previous);
$previousIsHumanProse = $previousBody !== null
&& trim($previousBody) !== ''
&& ! $this->isTodoCell($previousBody)
&& ! str_contains($previousBody, '대표 에러 없음');
if ($previousIsHumanProse) {
return str_replace($currentBody, trim($previousBody), $section);
}
return $section;
}
// 상태코드 키로 병합. `+` 는 왼쪽 우선이므로 기존 행을 먼저 둬서, 같은 상태코드면
// 사람이 쓴 구체적 조건이 자동 문구를 이긴다. 자동 추론에만 있는 상태코드는 새로 편입된다.
$merged = $previousRows + $this->errorTableRows($section);
@@ -1708,12 +1773,32 @@ class ApiDocScaffolder
private function locateSubsectionBody(string $block, string $label, array $terminators): ?array
{
$labelPos = strpos($block, $label);
$labelLength = strlen($label);
// 라벨 정확 일치 실패 시 베이스 라벨(굵은 제목)만으로 재시도한다.
// 봉투를 쓰지 않는 엔드포인트(레이아웃 JSON 을 root 에 그대로 내리는 미리보기 서빙 등)에서는
// `**응답 필드** (`data` 내부)` 라벨이 사실과 달라 사람이 접미를 떼어낸다. 정확 일치만 보면
// 기존 본문 구간을 못 찾아, 표가 아닌 **서술**로 채운 내용이 마커로 퇴행한다
// (표는 applyPreservedTableCells 가 행 키로 따로 지키지만, 서술은 이 경로가 유일하다).
// 라벨 줄 전체를 라벨 길이로 삼아야 본문 시작 위치가 어긋나지 않는다.
if ($labelPos === false) {
return null;
$base = Str::before($label, ' (');
if ($base === $label) {
return null;
}
$labelPos = strpos($block, $base);
if ($labelPos === false) {
return null;
}
$lineEnd = strpos($block, "\n", $labelPos);
$labelLength = ($lineEnd === false ? strlen($block) : $lineEnd) - $labelPos;
}
$bodyStart = $labelPos + strlen($label);
$bodyStart = $labelPos + $labelLength;
$candidates = [];
foreach ($terminators as $terminator) {
@@ -2033,6 +2118,9 @@ class ApiDocScaffolder
*/
public function insertExampleBlocks(string $content, array $exampleBlocks): array
{
// 줄 끝 정규화 — mergeDocument 와 같은 이유 (CRLF 체크아웃에서 줄 단위 매칭이 전부 실패).
$content = $this->normalizeEol($content);
$inserted = 0;
foreach ($exampleBlocks as $key => $blocks) {
+173
View File
@@ -193,6 +193,91 @@ class ParameterDescriber
'websocket_port' => 'WebSocket 포트 번호',
'websocket_scheme' => 'WebSocket 스킴 (http/https)',
'websocket_app_key' => 'WebSocket 앱 키',
'websocket_app_secret' => 'WebSocket 앱 시크릿',
'websocket_server_host' => 'WebSocket 서버 호스트 주소 (서버측 발행 대상)',
'websocket_server_port' => 'WebSocket 서버 포트 번호 (서버측 발행 대상)',
'websocket_server_scheme' => 'WebSocket 서버 스킴 (http/https — 서버측 발행 대상)',
'websocket_verify_ssl' => 'WebSocket 서버 SSL 인증서 검증 여부',
'log_driver' => '로그 드라이버 (single/daily/stack)',
'log_level' => '로그 레벨 (debug/info/warning/error 등)',
'log_days' => '로그 파일 보관 일수',
'search_engine_driver' => '검색 엔진 드라이버 (Scout 엔진 선택)',
'session_lifetime' => '세션 유효 시간 (분)',
// 레이아웃 JSON 스키마 최상위 키 (근거: docs/frontend/layout-json.md 필수/선택 필드 표,
// UpdateLayoutContentRequest 검증 규칙. 템플릿/레이아웃 저장 API 의 content.* 키.)
'layout_name' => '레이아웃 이름 (식별자 — 파일 경로 기반, 예: board/popular)',
'components' => '컴포넌트 트리 배열 (레이아웃이 렌더할 컴포넌트 정의)',
'data_sources' => 'API 데이터 소스 정의 배열 (id/endpoint/method)',
'init_actions' => '레이아웃 로드 시 실행할 초기화 액션 배열',
'named_actions' => '이름으로 호출 가능한 재사용 액션 정의 맵',
'defines' => '재사용 컴포넌트 정의 맵 (컴포넌트 트리에서 참조)',
'computed' => '계산된 값 정의 맵 (키 → 표현식)',
'modals' => '모달 컴포넌트 정의 배열',
'scripts' => '동적 로드할 외부 스크립트 배열',
'errorHandling' => '레이아웃 레벨 에러 핸들링 설정 (에러 코드별 핸들러 매핑)',
'globalHeaders' => '전역 HTTP 헤더 규칙 배열 (pattern + headers)',
'init_state' => '초기 상태 값 맵',
'initLocal' => 'API 응답을 `_local` 상태에 자동 복사할 키/경로',
'initGlobal' => 'API 응답을 `_global` 상태에 자동 복사할 키/경로',
'initIsolated' => 'API 응답을 `_isolated` 상태에 자동 복사할 키/경로',
'global_state' => '전역 상태 초기값 맵',
'slots' => '슬롯별 삽입 콘텐츠 맵 (베이스 레이아웃의 slot 위치에 주입)',
'extends' => '상속할 베이스 레이아웃 이름',
'transition_overlay' => '페이지 전환 오버레이 설정 (스켈레톤/스피너)',
'pageConfig' => '페이지 단위 설정 객체',
'error_config' => '에러 표시 설정 객체',
// SEO 페이지 생성기 (근거: docs/frontend/layout-json.md meta.seo 표,
// docs/backend/seo-system.md, Settings SEO 카탈로그)
'changefreq' => 'sitemap changefreq 값 (daily/weekly/monthly 등)',
'structured_data' => 'JSON-LD 구조화 데이터 정의',
'og' => 'Open Graph 메타태그 정의 맵',
'page_type' => 'SEO 템플릿 키를 결정하는 페이지 유형',
'toggle_setting' => 'SEO 활성화 여부를 결정하는 설정 경로',
'vars' => 'SEO 변수 선언 맵 (데이터 소스 값의 표현식 매핑)',
'meta_keywords' => 'SEO 메타 키워드 (검색엔진 노출 키워드, 쉼표 구분)',
'meta_title_suffix' => '모든 페이지 SEO 제목 뒤에 붙는 접미 문구',
'google_site_verification' => 'Google Search Console 사이트 소유 확인 코드',
'naver_site_verification' => '네이버 서치어드바이저 사이트 소유 확인 코드',
'twitter_default_card' => '기본 트위터 카드 유형 (summary 등)',
'twitter_default_site' => '기본 트위터 사이트 계정 (@handle)',
'og_image_default_width' => '기본 Open Graph 이미지 너비 (px)',
'og_image_default_height' => '기본 Open Graph 이미지 높이 (px)',
'sitemap_enabled' => 'sitemap.xml 생성 사용 여부',
'sitemap_schedule' => 'sitemap 자동 생성 주기',
'sitemap_schedule_time' => 'sitemap 자동 생성 시각',
'sitemap_cache_ttl' => 'sitemap 캐시 유효 시간 (초)',
'bot_detection_enabled' => '검색엔진 봇 감지 사용 여부 (봇 요청에 SEO 렌더링 적용)',
'bot_detection_library_enabled' => '봇 감지 라이브러리 사용 여부 (User-Agent 목록 대신 라이브러리 판정)',
'bot_user_agents' => '봇으로 판정할 User-Agent 목록',
'generator_enabled' => 'SEO 페이지 생성기 사용 여부',
'generator_content' => 'SEO 렌더링 본문 생성 방식',
// 캐시/보안/업로드 공통 (근거: Settings 카탈로그 advanced/security/upload 그룹)
'cache_enabled' => '캐시 사용 여부',
'cache_ttl' => '캐시 유효 시간 (초)',
'debug_mode' => '디버그 모드 사용 여부 (상세 오류 노출)',
'sql_query_log' => 'SQL 쿼리 로그 기록 여부',
'maintenance_mode' => '점검 모드 사용 여부 (사이트 접근 차단)',
'force_https' => 'HTTPS 강제 리다이렉트 여부',
'max_login_attempts' => '로그인 실패 허용 횟수 (초과 시 잠금)',
'login_lockout_time' => '로그인 잠금 지속 시간 (분)',
'login_attempt_enabled' => '로그인 시도 제한 사용 여부',
'auth_token_lifetime' => '인증 토큰 유효 시간 (분)',
'max_file_size' => '업로드 허용 최대 파일 크기',
'max_file_count' => '업로드 허용 최대 파일 개수',
'allowed_extensions' => '업로드 허용 확장자 목록',
'image_quality' => '이미지 리사이즈 시 압축 품질 (1~100)',
'image_max_width' => '이미지 리사이즈 최대 너비 (px)',
'image_max_height' => '이미지 리사이즈 최대 높이 (px)',
// 사이트 기본 정보 (근거: Settings general 그룹)
'site_url' => '사이트 기본 URL',
'site_description' => '사이트 설명',
'site_logo' => '사이트 로고 이미지',
'admin_email' => '관리자 이메일 주소',
'currency' => '통화 코드 (ISO 4217 — 예: KRW)',
];
/**
@@ -204,6 +289,94 @@ class ParameterDescriber
* @return string|null 설명 (없으면 null)
*/
public function describe(string $name, string $location = '', string $type = ''): ?string
{
// 중첩 필드(general.site_name, shipping.zipcode 등)는 전체명으로는 어떤 규칙에도
// 걸리지 않는다. 전체명 매칭을 먼저 시도하고, 실패하면 마지막 세그먼트(leaf)로
// 재시도한다 — 중첩 경로의 의미는 leaf 가 결정하고 부모는 그룹 라벨일 뿐이다.
// 다국어 로케일 접미(name.ko / alt_text.en)는 부모 필드의 로케일별 값이다.
if (str_contains($name, '.')) {
return $this->describeNested($name, $location, $type);
}
return $this->describeFlat($name, $location, $type);
}
/**
* 중첩 필드(`.` 포함)의 설명을 leaf 세그먼트로 유추합니다.
*
* @param string $name 중첩 파라미터명 (예: general.site_name)
* @param string $location 위치
* @param string $type 타입
* @return string|null 설명 (미매칭 시 null)
*/
private function describeNested(string $name, string $location, string $type): ?string
{
$leaf = Str::afterLast($name, '.');
$parent = Str::beforeLast($name, '.');
// 배열 원소 인덱스(items.*.name / ids.0) — leaf 가 인덱스면 그 앞을 leaf 로 본다.
if ($leaf === '*' || ctype_digit($leaf)) {
return $this->describe($parent, $location, $type);
}
// 로케일 접미(name.ko / alt_text.en): 부모 필드의 로케일별 값.
if (in_array($leaf, config('app.supported_locales', ['ko', 'en']), true)) {
$parentDesc = $this->describe($parent, $location, $type);
return $parentDesc === null
? null
: "{$parentDesc} — `{$leaf}` 로케일 값";
}
// 부모가 의미를 한정하는 그룹인 경우: leaf 만으로는 오설명이 된다.
// 예) seo_meta.title 은 "제목" 이 아니라 "SEO 메타 제목" 이다.
$scoped = $this->describeScopedLeaf($parent, $leaf);
if ($scoped !== null) {
return $scoped;
}
// leaf 로 재시도 (부모 그룹 라벨은 의미를 바꾸지 않는다).
return $this->describeFlat($leaf, $location, $type);
}
/**
* 부모 그룹이 의미를 한정하는 중첩 키의 설명을 반환합니다.
*
* 대부분의 설정 그룹(general/mail/upload 등)은 라벨일 뿐이라 leaf 가 의미를 결정하지만,
* SEO 메타 그룹의 `title`/`description`/`keywords` 는 일반 제목/설명이 아니라 검색엔진
* 노출용 메타 값이다. leaf 폴백만 태우면 "제목"/"설명" 으로 축소되어 오설명이 된다.
*
* @param string $parent 부모 경로 (예: seo_meta, content.meta.seo)
* @param string $leaf 마지막 세그먼트
* @return string|null 설명 (해당 없으면 null → 일반 leaf 폴백)
*/
private function describeScopedLeaf(string $parent, string $leaf): ?string
{
$parentLeaf = Str::afterLast($parent, '.');
// SEO 메타 그룹 (근거: Page\*Request 의 seo_meta.{title,description,keywords},
// 레이아웃 meta.seo — 검색엔진/소셜 공유 노출 값)
if (in_array($parentLeaf, ['seo', 'seo_meta'], true)) {
return match ($leaf) {
'title' => 'SEO 메타 제목 (검색엔진/소셜 공유 표시 제목)',
'description' => 'SEO 메타 설명 (검색엔진/소셜 공유 표시 요약)',
'keywords' => 'SEO 메타 키워드 (검색엔진 노출 키워드, 쉼표 구분)',
default => null,
};
}
return null;
}
/**
* 단일 세그먼트 파라미터의 설명을 반환합니다.
*
* @param string $name 파라미터명 (`.` 없음)
* @param string $location 위치 (path/query/body)
* @param string $type 타입
* @return string|null 설명 (미매칭 시 null)
*/
private function describeFlat(string $name, string $location, string $type): ?string
{
// path 파라미터는 언제나 리소스 식별자다(라우트 모델 바인딩). 이름이 무엇이든
// 정렬·필터 같은 조회 파라미터 의미를 가질 수 없으므로 이름 기반 규칙보다 앞서 처리한다.
@@ -105,6 +105,8 @@ class ResourceFieldDescriber
'install_blocked_reason' => '설치가 차단된 사유 (차단 없으면 null)',
// 감사/관계 공통 (도메인 무관하게 역할 고정 — 쓰기 응답에서 흔히 등장)
// creator/permissions 는 위 "관계/연관 객체" 블록에 이미 있으므로 여기서 재정의하지 않는다
// (PHP 배열은 뒤 키가 앞을 덮어 같은 값이 두 번 선언되는 상태였다).
'updated_by' => '최종 수정한 사용자 정보 (uuid/name — updated_by 관계 파생, 없으면 null)',
'user' => '대상 사용자 정보 객체 (uuid/name/email 등 — user 관계 파생)',
'roles' => '보유 역할 목록 (각 원소 id/name/permissions — roles 관계 파생)',
@@ -133,6 +135,47 @@ class ResourceFieldDescriber
'spec' => '스펙 정의 객체 (편집기/컴포넌트 선언 스키마 등)',
'source_meta' => '원천 메타데이터 객체 (출처·경로·해석 정보)',
'validation_summary' => '검증 결과 요약 객체 (통과/실패 건수 등)',
// 레이아웃 JSON 스키마 키 (근거: docs/frontend/layout-json.md 필수/선택 필드 표.
// 레이아웃 서빙/저장 응답의 content 하위 키 — 엔진 스키마 계약으로 의미 고정)
'$schema' => 'JSON 스키마 선언 URL (편집기/검증 도구용)',
'layout_name' => '레이아웃 이름 (식별자 — 파일 경로 기반, 예: board/popular)',
'components' => '컴포넌트 트리 (레이아웃이 렌더할 컴포넌트 정의)',
'data_sources' => 'API 데이터 소스 정의 배열 (id/endpoint/method)',
'modals' => '모달 컴포넌트 정의 배열',
'slots' => '슬롯별 삽입 콘텐츠 맵 (베이스 레이아웃의 slot 위치에 주입)',
'extends' => '상속하는 베이스 레이아웃 이름 (미상속 시 null)',
'schema' => '스키마 정의 객체',
'meta' => '메타 정보 객체 (title/description/seo 등)',
'pageConfig' => '페이지 단위 설정 객체',
'error_config' => '에러 표시 설정 객체',
'base_layouts' => '상속 가능한 베이스 레이아웃 목록',
'layout_versions' => '레이아웃 버전 이력 목록',
'preview' => '미리보기 정보 객체',
'vars' => 'SEO 변수 선언 맵 (데이터 소스 값의 표현식 매핑)',
'events' => '이벤트 정의 목록',
'toggle_settings' => '토글 가능한 설정 항목 목록',
'page_types' => 'SEO 템플릿 키로 사용 가능한 페이지 유형 목록',
'display_mode' => '표시 방식 (렌더/노출 모드 구분 값)',
'templateId' => '대상 템플릿의 식별자',
// 확장 manifest / 에셋 계약 (근거: module.json/plugin.json/template.json manifest,
// ExtensionBundleService 에셋 매니페스트)
'g7_version' => '요구하는 코어 최소 버전 (manifest requires.g7_version)',
'required_version' => '요구되는 최소 버전',
'manifest_source' => 'manifest 를 읽어온 출처 경로 (활성/_bundled 구분)',
'manifest_present' => 'manifest 파일 존재 여부',
'license' => '라이선스 식별자 (manifest license)',
'release_date' => '배포일',
'cache_version' => '에셋 캐시 무효화 버전 (번들 URL 파일명에 포함)',
'js' => '프론트엔드 JS 에셋 목록 (manifest assets 파생)',
'css' => '프론트엔드 CSS 에셋 목록 (manifest assets 파생)',
'externals' => '외부 의존 라이브러리 목록 (번들에서 제외되고 전역에서 해석)',
'routes' => '라우트 정의 목록',
'components_count' => '컴포넌트 개수 (집계)',
'theme_color' => '테마 대표 색상',
'popup_width' => '팝업 창 너비 (px)',
'popup_height' => '팝업 창 높이 (px)',
];
/**
@@ -143,6 +186,70 @@ class ResourceFieldDescriber
* @return string|null 설명 (없으면 null)
*/
public function describe(string $field, string $type = ''): ?string
{
// 중첩 필드(settings.general.site_name, meta.total 등)는 전체명으로 어떤 규칙에도
// 걸리지 않는다. 마지막 세그먼트(leaf)로 폴백한다 — 중첩 경로의 의미는 leaf 가
// 결정하고 상위 세그먼트는 그룹 라벨일 뿐이다. (ParameterDescriber 와 동일 계약)
if (str_contains($field, '.')) {
return $this->describeNested($field, $type);
}
return $this->describeFlat($field, $type);
}
/**
* 중첩 필드(`.` 포함)의 설명을 leaf 세그먼트로 유추합니다.
*
* @param string $field 중첩 필드명 (예: settings.general.site_name)
* @param string $type 실측 타입
* @return string|null 설명 (미매칭 시 null)
*/
private function describeNested(string $field, string $type): ?string
{
$leaf = Str::afterLast($field, '.');
$parent = Str::beforeLast($field, '.');
// 배열 원소 인덱스(items.*.name / items.0.name) — 그 앞을 leaf 로 본다.
if ($leaf === '*' || ctype_digit($leaf)) {
return $this->describe($parent, $type);
}
// 로케일 접미(name.ko): 부모 필드의 로케일별 값.
if (in_array($leaf, config('app.supported_locales', ['ko', 'en']), true)) {
$parentDesc = $this->describe($parent, $type);
return $parentDesc === null
? null
: "{$parentDesc} — `{$leaf}` 로케일 값";
}
// 부모가 의미를 한정하는 그룹(seo/seo_meta)의 title/description/keywords 는
// 일반 제목/설명이 아니라 검색엔진 노출용 메타 값이다 (ParameterDescriber 와 동일 계약).
$parentLeaf = Str::afterLast($parent, '.');
if (in_array($parentLeaf, ['seo', 'seo_meta'], true)) {
$scoped = match ($leaf) {
'title' => 'SEO 메타 제목 (검색엔진/소셜 공유 표시 제목)',
'description' => 'SEO 메타 설명 (검색엔진/소셜 공유 표시 요약)',
'keywords' => 'SEO 메타 키워드 (검색엔진 노출 키워드, 쉼표 구분)',
default => null,
};
if ($scoped !== null) {
return $scoped;
}
}
return $this->describeFlat($leaf, $type);
}
/**
* 단일 세그먼트 필드의 설명을 반환합니다.
*
* @param string $field 필드명 (`.` 없음)
* @param string $type 실측 타입
* @return string|null 설명 (미매칭 시 null)
*/
private function describeFlat(string $field, string $type): ?string
{
// sort_order 는 응답에서 표시 정렬 순서 값(정수 컬럼)으로 고정된다.
// 문자열이면 정렬 방향일 수 있어 도메인 특이 → TODO 유지.
+26 -1
View File
@@ -427,10 +427,35 @@ php artisan api:docgen-backfill-fields
□ 응답 필드 표가 envelope 의 data 내부 기준으로 작성되었는가?
□ 응답 예시(envelope 전문 JSON) 블록이 방출되었는가? (목록은 2항목 절단)
□ 훅 주입 파라미터가 있으면 주석 + 사람 보강이 되었는가?
□ TODO 마커가 모두 채워졌는가?
□ 미채움 마커 5종이 전수 0 인가? (아래 참조 — `TODO:` 만 세는 부분 집계 금지)
□ api:docgen --check 가 drift 0 인가?
```
### 미채움 마커 5종 (전수 0 이 완료 기준)
`api:docgen` 은 실측할 수 없는 자리에 "사람이 작성하세요" 마커를 남긴다. **실측 불가는 그 자리를 비워둘
사유가 아니다.** 컨트롤러/Resource/FormRequest/Enum/lang 을 읽어 코드 근거로 채우는 것이 규정이다.
| 마커 | 채울 것 | 근거 소스 |
| --- | --- | --- |
| `<!-- 실측 제외: {사유} — 응답 필드는 사람이 작성하세요. -->` | 응답 필드 표 | Resource `toArray()` 의 키 전수 + 타입, Enum `label()`/`variant()` |
| `<!-- 실측 제외: {사유} — 응답 예시는 사람이 작성하세요. -->` | 응답 예시 JSON | 컨트롤러의 `ResponseHelper::success/paginated/dataSource` 호출 형태 + Resource + lang 의 message 문구 |
| `<!-- TODO: 용도 -->` | 요청 파라미터 설명 셀 | FormRequest `rules()`/`messages()`, 설정 키는 관리자 설정 UI 다국어 라벨 |
| `<!-- TODO: 설명 -->` | 응답 필드 설명 셀 | Resource `toArray()` 파생 로직, 모델 `$casts`/관계, 마이그레이션 한국어 comment |
| `_대표 에러 없음 ... <!-- TODO: ... -->_` | 도메인 특이 에러 표 | 컨트롤러의 `abort()`/예외 throw, Service 의 커스텀 Exception |
도메인 무관하게 의미가 고정된 파라미터/필드는 문서에 손으로 쓰지 말고 `ParameterDescriber`/
`ResourceFieldDescriber` 사전에 등재한다 — 사전은 재생성에 영속하고 신규 문서에도 자동 적용된다.
반대로 **도메인 특이 의미를 공통 사전에 넣지 않는다** (오설명이 전 문서로 퍼진다).
전수 집계는 5종 마커를 한 번에 세야 한다. `TODO:` 만 세면 그 바깥의 미채움(`실측 제외` 등)이 집계에서
빠져 부분 집계가 완료로 통과한다:
```bash
grep -rcE "실측 제외:|TODO: 용도|TODO: 설명|실측 응답에 필드 없음|대표 에러 없음" \
--include=*.md docs/backend/api {modules,plugins}/_bundled/*/docs/api
```
---
## 관련 문서
+20 -6
View File
@@ -214,7 +214,7 @@ HTTP/1.1 200
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
| --- | --- | --- | --- | --- | --- |
| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) |
| ids | body | array | 예 | min 1 | 삭제할 활동 로그 ID 배열 (원소는 integer 이며 `activity_logs.id` 에 존재해야 함) |
**요청 예시**
@@ -234,11 +234,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| deleted_count | integer | `3` | 실제로 삭제된 활동 로그 건수 (`ActivityLogService::deleteMany()` 반환값 — 요청한 `ids` 중 삭제에 성공한 개수) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "선택한 활동 로그가 삭제되었습니다.",
"data": {
"deleted_count": 3
}
}
```
**에러 응답**
@@ -276,9 +292,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — 컨트롤러가 `success('activity_log.delete_success')` 를 데이터 인자 없이 호출하여 `data` 는 `null`)._
**응답 예시**
+32 -4
View File
@@ -27,7 +27,7 @@
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
| --- | --- | --- | --- | --- | --- |
| hash | path | string | 예 | — | 대상 리소스의 해시 식별자 |
| hash | path | string | 예 | — | 다운로드할 첨부파일의 해시 식별자 (`attachments.hash`) |
**요청 예시**
@@ -39,17 +39,45 @@ Accept: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 JSON 봉투(`data`)를 반환하지 않습니다. 성공 시 파일 바이너리 본문을 그대로 응답합니다 (실패 시에만 JSON 에러 봉투)._
| 응답 헤더 | 값 | 용도/설명 |
| --- | --- | --- |
| Content-Type | `image/png` 등 | 첨부파일의 MIME 타입 (`attachments.mime_type`) |
| Content-Disposition | `attachment; filename="원본파일명.pdf"` | 이미지가 아닌 파일에만 부여 — 원본 파일명으로 다운로드 |
| Cache-Control | `public, max-age=86400, immutable` (프로덕션) / `no-cache` (그 외) | 이미지 응답의 캐싱 정책. max-age 는 환경설정 `cache.layout_ttl` (기본 86400초) |
| Expires | `Wed, 15 Jul 2026 00:00:00 GMT` | 이미지 응답의 만료 시각 (현재 시각 + max-age) |
| ETag | `9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d` | 이미지 응답의 검증자 (파일 수정시각 + 크기의 MD5). 요청의 `If-None-Match` 와 일치하면 `304 Not Modified` |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200 OK
Content-Type: image/png
Cache-Control: public, max-age=86400, immutable
Expires: Wed, 15 Jul 2026 00:00:00 GMT
ETag: 9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d
<파일 바이너리>
```
이미지가 아닌 파일:
```http
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="manual.pdf"
<파일 바이너리>
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 304 | Not Modified | 이미지 응답에서 요청 `If-None-Match` 헤더가 현재 ETag 와 일치하는 경우 (본문 없음) |
| 403 | Forbidden | 첨부파일 접근 권한 없음 (`core.attachment.download` 훅 권한 미충족) 또는 스토리지에 실제 파일이 없는 경우 — `{"success": false, "message": "이 첨부파일에 대한 접근 권한이 없습니다."}` |
| 404 | Not Found | 해당 해시의 첨부파일이 존재하지 않는 경우 — `{"success": false, "message": "첨부파일을 찾을 수 없습니다."}` |
<!-- @generated:end -->
+154 -13
View File
@@ -75,11 +75,59 @@ example-key
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`AttachmentResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 첨부파일 ID (기본 키) |
| hash | string | `"aB3xY9kLmQ7z"` | URL용 고유 해시 (12자). 다운로드 URL 식별자로 사용 |
| original_filename | string | `"example.pdf"` | 업로드된 원본 파일명 |
| mime_type | string | `"application/pdf"` | MIME 타입 (예: `image/jpeg`, `application/pdf`) |
| size | integer | `102400` | 파일 크기 (바이트) |
| size_formatted | string | `"100 KB"` | 사람이 읽기 쉬운 형식으로 포맷된 파일 크기 (B/KB/MB/GB) |
| collection | string | `"default"` | 첨부파일 컬렉션/그룹명 (미지정 시 `default`) |
| order | integer | `0` | 컬렉션 내 정렬 순서 |
| download_url | string | `"/api/attachment/aB3xY9kLmQ7z"` | 해시 기반 다운로드 경로 |
| is_image | boolean | `false` | MIME 타입이 `image/` 로 시작하는지 여부 |
| meta | object\|null | `{"width": 800, "height": 600}` | 추가 메타데이터. 이미지 업로드 시 width/height 자동 기록, 그 외에는 빈 객체 |
| source_type | string | `"core"` | 첨부 생성 출처 (`core` / `module` / `plugin`) |
| source_identifier | string\|null | `"sirsoft-board"` | 모듈/플러그인 식별자 (코어 업로드 시 `null`) |
| creator | object\|없음 | `{"uuid": "...", "name": "관리자"}` | 업로더 정보(`uuid`, `name`). `creator` 관계가 eager load 된 경우에만 포함되며, 업로드 응답에는 포함되지 않음 |
| created_at | string | `"2026-07-14 10:23:41"` | 생성 일시 (사용자 타임존 기준 `Y-m-d H:i:s`) |
| updated_at | string | `"2026-07-14 10:23:41"` | 수정 일시 (사용자 타임존 기준 `Y-m-d H:i:s`) |
| is_owner | boolean | `true` | 요청자가 업로더(`created_by`)인지 여부 |
| abilities | object | `{"can_update": true, "can_delete": true}` | 요청자의 권한 맵 (`can_update` = `core.attachments.update`, `can_delete` = `core.attachments.delete`) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "파일이 업로드되었습니다.",
"data": {
"id": 1,
"hash": "aB3xY9kLmQ7z",
"original_filename": "example.pdf",
"mime_type": "application/pdf",
"size": 102400,
"size_formatted": "100 KB",
"collection": "default",
"order": 0,
"download_url": "/api/attachment/aB3xY9kLmQ7z",
"is_image": false,
"meta": {},
"source_type": "core",
"source_identifier": null,
"created_at": "2026-07-14 10:23:41",
"updated_at": "2026-07-14 10:23:41",
"is_owner": true,
"abilities": {
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -88,6 +136,7 @@ example-key
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.attachments.create`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 파일 저장/DB 기록 중 예외 발생 시 (`attachment.upload_failed` — "파일 업로드에 실패했습니다.") |
<!-- @generated:end -->
@@ -136,11 +185,88 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_배열 응답: `data` 는 `AttachmentResource` 객체의 배열입니다 (페이지네이션 없음). 각 항목의 필드는 단일 업로드 응답과 동일합니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| data[].id | integer | `1` | 첨부파일 ID (기본 키) |
| data[].hash | string | `"aB3xY9kLmQ7z"` | URL용 고유 해시 (12자) |
| data[].original_filename | string | `"photo1.jpg"` | 업로드된 원본 파일명 |
| data[].mime_type | string | `"image/jpeg"` | MIME 타입 |
| data[].size | integer | `204800` | 파일 크기 (바이트) |
| data[].size_formatted | string | `"200 KB"` | 포맷된 파일 크기 |
| data[].collection | string | `"default"` | 첨부파일 컬렉션/그룹명 |
| data[].order | integer | `0` | 컬렉션 내 정렬 순서 (배치 내 파일마다 순차 증가) |
| data[].download_url | string | `"/api/attachment/aB3xY9kLmQ7z"` | 해시 기반 다운로드 경로 |
| data[].is_image | boolean | `true` | MIME 타입이 `image/` 로 시작하는지 여부 |
| data[].meta | object\|null | `{"width": 1920, "height": 1080}` | 이미지 업로드 시 width/height 자동 기록 |
| data[].source_type | string | `"core"` | 첨부 생성 출처 (`core` / `module` / `plugin`) |
| data[].source_identifier | string\|null | `null` | 모듈/플러그인 식별자 |
| data[].created_at | string | `"2026-07-14 10:23:41"` | 생성 일시 (사용자 타임존 기준) |
| data[].updated_at | string | `"2026-07-14 10:23:41"` | 수정 일시 (사용자 타임존 기준) |
| data[].is_owner | boolean | `true` | 요청자가 업로더인지 여부 |
| data[].abilities | object | `{"can_update": true, "can_delete": true}` | 요청자의 권한 맵 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "파일이 일괄 업로드되었습니다.",
"data": [
{
"id": 1,
"hash": "aB3xY9kLmQ7z",
"original_filename": "photo1.jpg",
"mime_type": "image/jpeg",
"size": 204800,
"size_formatted": "200 KB",
"collection": "default",
"order": 0,
"download_url": "/api/attachment/aB3xY9kLmQ7z",
"is_image": true,
"meta": {
"width": 1920,
"height": 1080
},
"source_type": "core",
"source_identifier": null,
"created_at": "2026-07-14 10:23:41",
"updated_at": "2026-07-14 10:23:41",
"is_owner": true,
"abilities": {
"can_update": true,
"can_delete": true
}
},
{
"id": 2,
"hash": "Zq1WeRt5YuIo",
"original_filename": "photo2.jpg",
"mime_type": "image/jpeg",
"size": 153600,
"size_formatted": "150 KB",
"collection": "default",
"order": 1,
"download_url": "/api/attachment/Zq1WeRt5YuIo",
"is_image": true,
"meta": {
"width": 1280,
"height": 720
},
"source_type": "core",
"source_identifier": null,
"created_at": "2026-07-14 10:23:41",
"updated_at": "2026-07-14 10:23:41",
"is_owner": true,
"abilities": {
"can_update": true,
"can_delete": true
}
}
]
}
```
**에러 응답**
@@ -149,6 +275,7 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.attachments.create`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 배치 중 파일 저장/DB 기록 예외 발생 시 (`attachment.upload_failed` — "파일 업로드에 실패했습니다.") |
<!-- @generated:end -->
@@ -187,19 +314,26 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 데이터를 반환하지 않습니다 (`data` 는 `null`, 성공 메시지만 반환)._
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "파일 순서가 변경되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.attachments.create`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 403 | Forbidden | 요구 권한(`core.attachments.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `order[].id` 가 존재하지 않는 첨부파일이면 "존재하지 않는 첨부파일입니다." |
| 500 | Internal Server Error | 순서 갱신 중 예외 발생 시 (`attachment.reorder_failed` — "파일 순서 변경에 실패했습니다.") |
<!-- @generated:end -->
@@ -229,20 +363,27 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 데이터를 반환하지 않습니다 (`data` 는 `null`, 성공 메시지만 반환)._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "파일이 삭제되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 삭제 처리가 실패로 반환된 경우 (`attachment.delete_failed` — "파일 삭제에 실패했습니다.") |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.attachments.create`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 403 | Forbidden | 요구 권한(`core.attachments.delete`)이 없는 경우 |
| 404 | Not Found | `{attachment}` 에 해당하는 첨부파일이 없는 경우 (라우트 모델 바인딩 실패) |
| 500 | Internal Server Error | 파일/DB 삭제 중 예외 발생 시 (`attachment.delete_failed` — "파일 삭제에 실패했습니다.") |
<!-- @generated:end -->
+297 -36
View File
@@ -38,9 +38,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `AuthController@logout` 이 `success('auth.logout_success')` 를 인자 없이 호출하므로 `data` 는 `null`)._
**응답 예시**
@@ -285,18 +283,59 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-500 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`AuthService::login()` 이 반환한 배열 — `user` 만 `UserResource` 로 감싼다)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| user | object | `{"uuid":"a234c2b1-…","name":"관리자","is_admin":true, …}` | 로그인한 관리자 정보 (`UserResource` — 필드 전수는 `GET /api/admin/auth/user` 응답 필드 표와 동일) |
| token | string | `75\|WgPUplvLGTv8YIj4507uIR6dEOHTXyNUed…` | 발급된 Sanctum 접근 토큰 평문 (이후 `Authorization: Bearer` 헤더로 사용, 발급 시 1회만 노출) |
| token_type | string | `Bearer` | 토큰 타입 (항상 `Bearer` — `AuthService::login()` 이 상수로 반환) |
**응답 예시**
<!-- 실측 제외: http-500 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "관리자 로그인이 성공했습니다.",
"data": {
"user": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"status": "active",
"is_admin": true,
"roles": [
{ "id": 1, "identifier": "admin", "name": "관리자" }
],
"is_owner": true,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
}
},
"token": "{MASKED}",
"token_type": "Bearer"
}
}
```
> `user` 객체는 지면 절약을 위해 축약했습니다. 실제로는 `GET /api/admin/auth/user` 의 `data` 와 동일한 `UserResource` 필드 전수가 내려옵니다.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 401 | Unauthenticated | 이메일/비밀번호가 일치하지 않는 경우 (`auth.login_failed`) |
| 403 | Forbidden | 자격 증명은 맞지만 관리자 역할이 아닌 경우 (`auth.admin_required`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 423 | Locked | 로그인 실패 누적으로 계정이 잠긴 경우 (`auth.account_locked` — `error.locked_until`, `error.retry_after_seconds` 포함) |
<!-- @generated:end -->
@@ -336,18 +375,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `AuthController@forgotPassword` 가 `success('auth.password_reset_email_sent')` 를 인자 없이 호출하므로 `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "비밀번호 재설정 이메일이 발송되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우, 또는 등록되지 않은 이메일인 경우 (`auth.password_reset_failed` — `error.errors.email` 에 `auth.email_not_registered` 메시지) |
<!-- @generated:end -->
@@ -387,18 +435,56 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-500 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`AuthService::login()` 이 반환한 배열 — `user` 만 `UserResource` 로 감싼다)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| user | object | `{"uuid":"a234c2b1-…","name":"홍길동","is_admin":false, …}` | 로그인한 사용자 정보 (`UserResource` — 필드 전수는 `GET /api/auth/user` 응답 필드 표의 기본(코어) 필드와 동일) |
| token | string | `75\|WgPUplvLGTv8YIj4507uIR6dEOHTXyNUed…` | 발급된 Sanctum 접근 토큰 평문 (이후 `Authorization: Bearer` 헤더로 사용, 발급 시 1회만 노출) |
| token_type | string | `Bearer` | 토큰 타입 (항상 `Bearer`) |
**응답 예시**
<!-- 실측 제외: http-500 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "로그인이 성공했습니다.",
"data": {
"user": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"language": "ko",
"status": "active",
"is_admin": false,
"is_owner": true,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
}
},
"token": "{MASKED}",
"token_type": "Bearer"
}
}
```
> `user` 객체는 지면 절약을 위해 축약했습니다. 실제로는 `UserResource` 필드 전수가 내려옵니다.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 401 | Unauthenticated | 이메일/비밀번호가 일치하지 않거나 계정 상태가 활성이 아닌 경우 (`auth.login_failed`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 423 | Locked | 로그인 실패 누적으로 계정이 잠긴 경우 (`auth.account_locked` — `error.locked_until`, `error.retry_after_seconds` 포함) |
<!-- @generated:end -->
@@ -491,9 +577,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `AuthController@logout` 이 `success('auth.logout_success')` 를 인자 없이 호출하므로 `data` 는 `null`)._
**응답 예시**
@@ -579,18 +663,56 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`AuthService::register()` 이 반환한 배열 — `user` 만 `UserResource` 로 감싼다). 성공 시 HTTP 201._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| user | object | `{"uuid":"a234c2b1-…","name":"홍길동","status":"active", …}` | 생성된 사용자 정보 (`UserResource`. `status` 는 `active`, 가입 후 본인인증 정책이 걸린 경우 `pending_verification`) |
| token | string | `75\|WgPUplvLGTv8YIj4507uIR6dEOHTXyNUed…` | 가입 즉시 발급되는 Sanctum 접근 토큰 평문 (가입 직후 로그인 상태로 이어짐) |
| token_type | string | `Bearer` | 토큰 타입 (항상 `Bearer`) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "회원가입이 성공했습니다.",
"data": {
"user": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"nickname": null,
"email": "apidoc-sample-user@example.com",
"language": "ko",
"status": "active",
"is_admin": false,
"is_owner": true,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
}
},
"token": "{MASKED}",
"token_type": "Bearer"
}
}
```
> `user` 객체는 지면 절약을 위해 축약했습니다. 실제로는 `UserResource` 필드 전수가 내려옵니다.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`auth.register_failed` — `error.errors` 에 필드별 메시지) |
| 428 | Precondition Required | 가입 전 본인인증 정책이 매칭되었으나 유효한 `verification_token` 이 없는 경우 (`core.auth.before_register` 훅) |
<!-- @generated:end -->
@@ -632,18 +754,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `AuthController@resetPassword` 가 `success('auth.password_reset_success')` 를 인자 없이 호출하므로 `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "비밀번호가 성공적으로 재설정되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반했거나, 토큰이 유효하지 않거나 만료된 경우 (`auth.password_reset_failed` — `error.errors` 에 필드별 메시지) |
<!-- @generated:end -->
@@ -813,18 +944,33 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`AuthService::validateResetToken()` 의 반환 배열)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| valid | boolean | `true` | 토큰 유효 여부. 성공 응답에서는 항상 `true` (유효하지 않으면 컨트롤러가 422 로 전환하므로 이 필드가 `false` 인 200 응답은 없음) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"valid": true
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터 검증 위반, 또는 토큰이 유효하지 않거나 만료된 경우 / 미등록 이메일 (`auth.reset_token_invalid` — `error.errors.token` 에 사유 메시지) |
<!-- @generated:end -->
@@ -854,11 +1000,21 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `AuthController@logout` 이 `success('auth.logout_success')` 를 인자 없이 호출하므로 `data` 는 `null`). 공용 `POST /api/auth/logout` 과 동일한 컨트롤러 메서드._
**응답 예시**
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "로그아웃이 성공했습니다.",
"data": null
}
```
**에러 응답**
@@ -895,11 +1051,21 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `AuthController@logoutFromAllDevices` 가 `success('auth.logout_all_devices_success')` 를 인자 없이 호출하므로 `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "모든 디바이스에서 로그아웃되었습니다.",
"data": null
}
```
**에러 응답**
@@ -936,11 +1102,47 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`AuthService::refreshToken()` 반환 배열 — 공용 `POST /api/admin/auth/refresh` 와 동일 shape)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| user | object | `{"uuid":"a234c2b1-…","name":"홍길동", …}` | 토큰을 갱신한 사용자 정보 (`UserResource`) |
| token | string | `75\|WgPUplvLGTv8YIj4507uIR6dEOHTXyNUed…` | 새로 발급된 Sanctum 접근 토큰 평문 (기존 토큰은 폐기됨) |
| token_type | string | `Bearer` | 토큰 타입 (항상 `Bearer`) |
**응답 예시**
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"user": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"status": "active",
"is_admin": false,
"is_owner": true,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
}
},
"token": "{MASKED}",
"token_type": "Bearer"
}
}
```
> `user` 객체는 지면 절약을 위해 축약했습니다. 실제로는 `UserResource` 필드 전수가 내려옵니다.
**에러 응답**
@@ -977,11 +1179,70 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드. 컨트롤러 메서드가 공용 `GET /api/auth/user` 와 동일한 `AuthController@user` 이므로, 응답은 `UserResource::toAuthArray()` 산물(코어 필드 + `core.user.filter_resource_data` 로 확장이 병합한 필드)로 그 문서의 응답 필드 표와 동일합니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| uuid | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) |
| name | string | `API 문서 샘플 사용자` | 사용자 이름 |
| nickname | string \| null | `song.hyunji` | 닉네임 (미설정 시 null) |
| email | string | `apidoc-sample-user@example.com` | 이메일 주소 |
| language | string | `ko` | 사용자 언어 설정 |
| status | string | `active` | 계정 상태 (active / inactive / blocked / withdrawn / pending_verification) |
| is_admin | boolean | `false` | 관리자 역할 보유 여부 |
| roles | array | `[{"id":2,"identifier":"user","name":"일반회원"}]` | 부여된 역할 목록 (id/identifier/name) |
| permissions | array | `[{"id":81,"identifier":"sirsoft-ecommerce.user-products.read","name":"상품 조회"}]` | 역할 경유 권한 목록 (id/identifier/name) |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_read":true,"can_update":true, …}` | 이 리소스에 수행 가능한 작업 불리언 맵 |
| (확장 병합 필드) | — | `notify_*`, `marketing_consent*`, `ecommerce_*` 등 | marketing 플러그인·ecommerce 모듈이 `core.user.filter_resource_data` 필터로 병합 — 전수는 `GET /api/auth/user` 응답 필드 표 참조 |
> 필드 전수(코어 + 확장 병합)는 `GET /api/auth/user` 의 응답 필드 표와 동일하므로 그 표를 SSoT 로 참조합니다. 게스트(비인증) 요청은 `optional.sanctum` 을 통과하지만 `request()->user()` 가 없으므로 이 경로는 인증 사용자 컨텍스트에서만 사용자 객체를 반환합니다.
**응답 예시**
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"nickname": "song.hyunji",
"email": "apidoc-sample-user@example.com",
"language": "ko",
"status": "active",
"is_admin": false,
"roles": [
{ "id": 2, "identifier": "user", "name": "일반회원" }
],
"permissions": [],
"is_owner": true,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
},
"notify_post_complete": false,
"notify_post_reply": false,
"notify_comment": false,
"notify_reply_comment": false,
"ecommerce_mileage": {
"enabled": false
},
"ecommerce_preferred_currency": null,
"ecommerce_preferred_shipping_country": null,
"ecommerce_preferred_shipping_country_name": null
}
}
```
> 지면 절약을 위해 축약했습니다. 실제 응답은 `GET /api/auth/user` 의 응답 예시와 동일한 필드 전수를 포함합니다.
**에러 응답**
+27 -4
View File
@@ -38,17 +38,25 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "프로필 이미지가 성공적으로 삭제되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 404 | Not Found | 삭제할 아바타 첨부파일이 없는 경우 (`user.avatar_not_found` — "삭제할 프로필 이미지가 없습니다.") |
| 500 | Server Error | 첨부파일/실제 파일 삭제 중 예외 발생 (`user.avatar_delete_failed` — "프로필 이미지 삭제에 실패했습니다.") |
<!-- @generated:end -->
@@ -86,11 +94,25 @@ Content-Type: image/png
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| avatar | string\|null | `/api/attachment/9f1c2b7d8e...` | 업로드된 아바타의 다운로드 URL (`Attachment.download_url` = `/api/attachment/{hash}`). 레거시 `avatar` 컬럼만 있는 경우 `storage/attachments/avatars/{파일명}` 형태의 절대 URL |
| attachment_id | integer | `1` | 생성된 첨부파일(Attachment) 레코드의 기본 키 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "프로필 이미지가 성공적으로 업로드되었습니다.",
"data": {
"avatar": "/api/attachment/9f1c2b7d8e3a4c5b6d7e8f90a1b2c3d4",
"attachment_id": 1
}
}
```
**에러 응답**
@@ -98,6 +120,7 @@ Content-Type: image/png
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Server Error | 파일 저장/첨부파일 생성 중 예외 발생 (`user.avatar_upload_failed` — "프로필 이미지 업로드에 실패했습니다.") |
<!-- @generated:end -->
+11 -2
View File
@@ -37,17 +37,26 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 G7 공통 응답 봉투(`success`/`message`/`data`)를 사용하지 않습니다. 라우트 클로저가 `Broadcast::auth($request)` 의 반환값을 그대로 응답하므로, 응답 본문은 Laravel 브로드캐스팅 드라이버(Reverb — Pusher 프로토콜 호환)가 생성하는 채널 인증 페이로드입니다. 등록된 채널(`routes/channels.php`, 모듈/플러그인 `getChannels()`)은 모두 boolean 을 반환하는 private 채널이므로 아래 필드만 반환됩니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| auth | string | `앱키:서명` 형식 문자열 | 클라이언트가 WebSocket 서버(Reverb)에 프라이빗 채널 구독을 요청할 때 제시하는 인증 서명. `{reverb_app_key}:{HMAC-SHA256(socket_id:channel_name)}` 형태이며, 브로드캐스팅 클라이언트 라이브러리가 자동으로 소비합니다 |
**응답 예시**
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
```json
{
"auth": "app-key:5f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8"
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 웹소켓 사용 OFF(`broadcasting.default === 'null'`) 로 채널 인증이 거부된 경우, 또는 요청한 채널의 인증 콜백이 `false` 를 반환한 경우(권한 부족·타 사용자 채널 구독 시도 등) |
<!-- @generated:end -->
+22 -2
View File
@@ -95,11 +95,31 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| update_available | boolean | `false` | 새 버전 존재 여부 (최신 릴리스 버전이 현재 버전보다 높으면 `true`) |
| current_version | string | `7.0.3` | 현재 설치된 코어 버전 (`config('app.version')`) |
| latest_version | string | `7.0.3` | GitHub 릴리스에서 조회한 최신 코어 버전 (조회 값이 없으면 현재 버전과 동일) |
| github_url | string | `https://github.com/gnuboard/g7` | 업데이트 조회 대상 GitHub 저장소 URL (`config('app.update.github_url')`) |
> 조회 실패 시(GitHub 접속 불가 등)에는 422 응답이 반환되며, `error` 객체에 `reason`(실패 사유) · `current_version` · `github_url` 이 담깁니다.
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "업데이트 확인이 완료되었습니다.",
"data": {
"update_available": false,
"current_version": "7.0.3",
"latest_version": "7.0.3",
"github_url": "https://github.com/gnuboard/g7"
}
}
```
**에러 응답**
+52 -2
View File
@@ -141,9 +141,21 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
_목록 응답: `data` 배열 항목의 필드. `data` 는 `core.dashboard.alerts` 필터 훅의 결과이며, 알릴 항목이 없으면 빈 배열(`[]`)입니다. 코어 기본 리스너(`ExtensionCompatibilityAlertListener`)가 주입하는 항목의 필드는 다음과 같습니다._
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | string | `compat_plugins_sirsoft-gdpr` | 알림 식별자 (`compat_{type}_{identifier}` = 자동 비활성화, `recover_{type}_{identifier}` = 재호환. 알림 닫기(dismiss) 상태 판정 키) |
| type | string | `warning` | 알림 등급 (`warning`: 코어 비호환 자동 비활성화, `info`: 재호환 복구 가능) |
| subtype | string | `incompatible_core` | 알림 세부 분류 (`incompatible_core`: 코어 버전 비호환으로 자동 비활성화됨, `recovery_available`: 코어 업그레이드 후 다시 활성화 가능) |
| icon | string | `exclamation-triangle` | 아이콘 식별자 (warning: `exclamation-triangle`, info: `check-circle`) |
| title | string | `플러그인 "sirsoft-gdpr" 자동 비활성화됨` | 알림 제목 (다국어 문구 — `extensions.alerts.incompatible_deactivated` / `recovered_title`) |
| message | string | `필요 버전: 7.0.0-beta.9, 현재 설치됨: 7.0.0-beta.8` | 알림 본문 (다국어 문구 — `extensions.alerts.incompatible_message` / `recovered_body`) |
| extension_type | string | `plugin` | 대상 확장 종류 (`module` / `plugin` / `template`) |
| identifier | string | `sirsoft-gdpr` | 대상 확장 식별자 |
| recover_endpoint | string | `/api/admin/extensions/plugin/sirsoft-gdpr/recover` | 재활성화 호출 엔드포인트 (재호환 알림 `subtype=recovery_available` 에만 존재) |
| time | string\|null | `3시간 전` | 자동 비활성화 시각의 상대 표시 (diffForHumans() 산물, 비활성화 시각이 없으면 `null`) |
| read | boolean | `false` | 읽음 여부 (주입 시점에는 항상 `false`) |
**응답 예시**
@@ -151,6 +163,8 @@ Authorization: Bearer {YOUR_TOKEN}
HTTP/1.1 200
```
알릴 항목이 없는 경우 (기본):
```json
{
"success": true,
@@ -159,6 +173,42 @@ HTTP/1.1 200
}
```
코어 비호환으로 자동 비활성화된 확장이 있는 경우:
```json
{
"success": true,
"message": "시스템 알림을 성공적으로 조회했습니다.",
"data": [
{
"id": "compat_plugins_sirsoft-gdpr",
"type": "warning",
"subtype": "incompatible_core",
"icon": "exclamation-triangle",
"title": "플러그인 \"sirsoft-gdpr\" 자동 비활성화됨",
"message": "필요 버전: 7.0.0-beta.9, 현재 설치됨: 7.0.0-beta.8",
"extension_type": "plugin",
"identifier": "sirsoft-gdpr",
"time": "3시간 전",
"read": false
},
{
"id": "recover_modules_sirsoft-board",
"type": "info",
"subtype": "recovery_available",
"icon": "check-circle",
"title": "모듈 \"sirsoft-board\" 다시 호환 가능",
"message": "코어 업그레이드 후 호환됩니다 (이전 요구: 7.0.0-beta.9). 다시 활성화할 수 있습니다.",
"extension_type": "module",
"identifier": "sirsoft-board",
"recover_endpoint": "/api/admin/extensions/module/sirsoft-board/recover",
"time": "2일 전",
"read": false
}
]
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
+40 -5
View File
@@ -102,11 +102,27 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| alert_id | string | `compat_plugins_sirsoft-tosspayments` | dismiss 처리된 자동 비활성화 알림의 식별자 (`compat_{type}s_{identifier}` 형식). 재호환 알림(`recover_{type}s_{identifier}`)도 함께 dismiss 되지만 응답에는 포함되지 않음 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "알림을 닫았습니다.",
"data": {
"alert_id": "compat_plugins_sirsoft-tosspayments"
}
}
```
**에러 응답**
@@ -145,11 +161,29 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| extension_type | string | `plugin` | 복구(재활성화)된 확장의 타입 (`module` \| `plugin` \| `template`) — 요청 path의 `{type}` 을 그대로 반환 |
| identifier | string | `sirsoft-tosspayments` | 복구된 확장의 식별자 — 요청 path의 `{identifier}` 를 그대로 반환 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "확장이 다시 활성화되었습니다.",
"data": {
"extension_type": "plugin",
"identifier": "sirsoft-tosspayments"
}
}
```
**에러 응답**
@@ -157,7 +191,8 @@ Authorization: Bearer {YOUR_TOKEN}
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.plugins.activate`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 (`extensions.errors.not_found`) |
| 422 | Unprocessable Entity | 재검증 결과 여전히 코어 버전이 요구 버전에 미달하는 경우 — 글로벌 핸들러가 `core_version_mismatch` 로 변환 |
<!-- @generated:end -->
+628 -68
View File
@@ -93,7 +93,7 @@ HTTP/1.1 200
```json
{
"success": true,
"message": "messages.success",
"message": "성공적으로 처리되었습니다.",
"data": {
"data": [
{
@@ -221,7 +221,7 @@ HTTP/1.1 200
```json
{
"success": true,
"message": "messages.success",
"message": "성공적으로 처리되었습니다.",
"data": {
"purged_count": 200,
"older_than_days": 1
@@ -414,11 +414,99 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (생성된 정의를 `IdentityMessageDefinitionResource` 로 직렬화, `templates` eager load 포함). 성공 시 HTTP 201._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `13` | 기본 키 (내부 식별자) |
| provider_id | string | `g7:core.mail` | 이 정의가 속한 IDV 프로바이더 ID |
| scope_type | string | `policy` | 메시지 정의 스코프 (관리자 생성은 항상 `policy`) |
| scope_value | string | `admin.custom_action` | 스코프 값 (source_type='admin' 인 IdentityPolicy.key) |
| name | object | `{"ko":"...","en":"..."}` | 다국어 표시명 |
| description | object | `{"ko":"...","en":"..."}` | 다국어 설명 (미전달 시 null) |
| channels | array | `["mail"]` | 이 정의가 지원하는 활성 채널 목록 |
| variables | array | `[{"key":"code","description":"인증 코드"}]` | 템플릿 치환 가능 변수 메타데이터 목록 (미전달 시 null) |
| extension_type | string | `core` | 이 리소스를 소유한 확장의 타입 (관리자 생성은 core) |
| extension_identifier | string | `core` | 이 리소스를 소유한 확장의 식별자 |
| is_active | boolean | `true` | 활성 여부 |
| is_default | boolean | `false` | 시드 기본 정의 여부 (관리자 생성 정의는 false → 삭제 가능) |
| user_overrides | array | `null` | 운영자가 시드 기본값에서 수정한 필드 경로 목록 (신규 생성 직후 null) |
| templates | array | `[{"id":13,"definition_id":13,"channel":"mail","subject":{…},"body":{…}}]` | 함께 생성된 채널별 하위 템플릿 목록 |
| created_at | string | `2026-07-08 10:43:32` | 생성 일시 |
| updated_at | string | `2026-07-08 10:43:32` | 최종 수정 일시 |
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "IDV 메시지 정의가 생성되었습니다.",
"data": {
"id": 13,
"provider_id": "g7:core.mail",
"scope_type": "policy",
"scope_value": "admin.custom_action",
"name": {
"ko": "커스텀 민감작업 인증",
"en": "Custom Sensitive Action"
},
"description": {
"ko": "운영자가 등록한 정책 전용 인증 메일",
"en": "Policy-specific verification mail added by admin"
},
"channels": [
"mail"
],
"variables": [
{
"key": "code",
"description": "인증 코드 (text_code 흐름)"
}
],
"extension_type": "core",
"extension_identifier": "core",
"is_active": true,
"is_default": false,
"user_overrides": null,
"templates": [
{
"id": 13,
"definition_id": 13,
"channel": "mail",
"subject": {
"ko": "[{app_name}] 인증 코드",
"en": "[{app_name}] Verification Code"
},
"body": {
"ko": "<p>인증 코드: {code}</p>",
"en": "<p>Your code: {code}</p>"
},
"is_active": true,
"is_default": false,
"user_overrides": null,
"updated_by": null,
"created_at": "2026-07-08 10:43:32",
"updated_at": "2026-07-08 10:43:32",
"abilities": {
"can_update": true,
"can_delete": true
}
}
],
"created_at": "2026-07-08 10:43:32",
"updated_at": "2026-07-08 10:43:32",
"abilities": {
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -427,6 +515,7 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 정의 생성 중 예외 발생 (`IDV 메시지 정의 생성에 실패했습니다.`) |
<!-- @generated:end -->
@@ -456,20 +545,26 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — 컨트롤러가 `$this->success(__('identity_message.definition_deleted'))` 를 인자 1개로 호출)._
**응답 예시**
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "IDV 메시지 정의가 삭제되었습니다."
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우, 또는 대상 정의가 시드 기본 정의(`is_default=true`)인 경우 (`시드된 기본 메시지 정의는 삭제할 수 없습니다.`) |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 삭제 중 예외 발생 (`IDV 메시지 정의 삭제에 실패했습니다.`) |
<!-- @generated:end -->
@@ -670,20 +765,111 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (수정된 정의를 `IdentityMessageDefinitionResource` 로 직렬화, `templates` eager load 포함 — GET 상세 응답과 동일 구조)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| provider_id | string | `g7:core.mail` | IDV 프로바이더 ID |
| scope_type | string | `purpose` | 메시지 정의 스코프 (provider_default\|purpose\|policy) |
| scope_value | string | `checkout_verification` | 범위 값 (provider_default 빈 문자열 / purpose 키 / policy 키) |
| name | object | `{"ko":"결제 시 본인 확인","en":"Checkout Verification"}` | 다국어 표시명 (수정 반영) |
| description | object | `{"ko":"...","en":"..."}` | 다국어 설명 (수정 반영) |
| channels | array | `["mail"]` | 활성 채널 목록 (수정 반영) |
| variables | array | `[{"key":"code","description":"인증 코드 (text_code 흐름)"}]` | 템플릿 치환 변수 메타데이터 목록 |
| extension_type | string | `module` | 이 리소스를 소유한 확장의 타입 |
| extension_identifier | string | `sirsoft-ecommerce` | 이 리소스를 소유한 확장의 식별자 |
| is_active | boolean | `true` | 활성 여부 (수정 반영) |
| is_default | boolean | `true` | 시드 기본 정의 여부 |
| user_overrides | array | `["name","channels"]` | 이번 수정으로 append 된 운영자 재정의 필드 목록 (시더 재실행 시 보존 대상) |
| templates | array | `[{"id":1,"definition_id":1,"channel":"mail",…}]` | 채널별 하위 템플릿 목록 |
| created_at | string | `2026-07-08 10:43:32` | 생성 일시 |
| updated_at | string | `2026-07-08 12:14:36` | 최종 수정 일시 |
| abilities | object | `{"can_update":true,"can_delete":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "IDV 메시지 정의가 수정되었습니다.",
"data": {
"id": 1,
"provider_id": "g7:core.mail",
"scope_type": "purpose",
"scope_value": "checkout_verification",
"name": {
"ko": "결제 시 본인 확인",
"en": "Checkout Verification"
},
"description": {
"ko": "결제 진행 전 본인/성인 확인 인증 코드 메일",
"en": "Identity/adult verification code mail before checkout"
},
"channels": [
"mail"
],
"variables": [
{
"key": "code",
"description": "인증 코드 (text_code 흐름)"
}
],
"extension_type": "module",
"extension_identifier": "sirsoft-ecommerce",
"is_active": true,
"is_default": true,
"user_overrides": [
"name"
],
"templates": [
{
"id": 1,
"definition_id": 1,
"channel": "mail",
"subject": {
"ko": "[{app_name}] 결제 본인 확인 인증 코드",
"en": "[{app_name}] Checkout Verification Code"
},
"body": {
"ko": "<h1>결제 본인 확인</h1>…",
"en": "<h1>Checkout Verification</h1>…"
},
"is_active": true,
"is_default": true,
"user_overrides": null,
"updated_by": null,
"created_at": "2026-07-08 10:43:32",
"updated_at": "2026-07-08 10:43:32",
"abilities": {
"can_update": true,
"can_delete": true
}
}
],
"created_at": "2026-07-08 10:43:32",
"updated_at": "2026-07-08 12:14:36",
"abilities": {
"can_update": true,
"can_delete": false
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 수정 중 예외 발생 (`IDV 메시지 정의 수정에 실패했습니다.`) |
<!-- @generated:end -->
@@ -1002,19 +1188,38 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`IdentityMessageTemplate::replaceVariables()` 의 반환값 — 저장 없이 렌더 결과만 반환)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| subject | string | `[G7] 결제 본인 확인 인증 코드` | 지정 로케일 제목에 `{key}` 변수를 `data` 값으로 치환한 결과 문자열 |
| body | string | `<h1>결제 본인 확인</h1><p>… 123456 …</p>` | 지정 로케일 본문(HTML)에 `{key}` 변수를 치환한 결과 문자열 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "IDV 메시지 템플릿 미리보기를 생성했습니다.",
"data": {
"subject": "[G7] 결제 본인 확인 인증 코드",
"body": "<h1>결제 본인 확인</h1><p>결제를 진행하기 위해 본인 확인이 필요합니다. 아래 인증 코드를 입력해 주세요.</p><p style=\"font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;\">123456</p><p>이 코드는 <strong>5분</strong> 후 만료됩니다.</p>"
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | `template_id` 에 해당하는 템플릿이 없거나(`findOrFail`) 렌더 중 예외 발생 (`IDV 메시지 템플릿 미리보기 생성에 실패했습니다.`) |
<!-- @generated:end -->
@@ -1060,20 +1265,71 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (수정된 템플릿을 `IdentityMessageTemplateResource` 로 직렬화 — templates reset 응답과 동일 구조)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| definition_id | integer | `1` | 이 템플릿이 속한 메시지 정의 ID |
| channel | string | `mail` | 메시지 템플릿 채널 (mail 현재 / sms 등 향후) |
| subject | object | `{"ko":"[{app_name}] 결제 본인 확인 인증 코드","en":"…"}` | 다국어 제목 (수정 반영, mail 채널에서만 의미) |
| body | object | `{"ko":"<h1>결제 본인 확인</h1>…","en":"…"}` | 다국어 본문 (수정 반영) |
| is_active | boolean | `true` | 활성 여부 (수정 반영) |
| is_default | boolean | `false` | 시더 기본값 유지 여부 (운영자 수정 시 false) |
| user_overrides | array | `["subject","body"]` | 이번 수정으로 append 된 운영자 재정의 필드 목록 |
| updated_by | null | `null` | 최종 수정한 사용자 정보 (uuid/name — 없으면 null) |
| created_at | string | `2026-07-08 10:43:32` | 생성 일시 |
| updated_at | string | `2026-07-08 12:14:36` | 최종 수정 일시 |
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "IDV 메시지 템플릿이 수정되었습니다.",
"data": {
"id": 1,
"definition_id": 1,
"channel": "mail",
"subject": {
"ko": "[{app_name}] 결제 본인 확인 인증 코드",
"en": "[{app_name}] Checkout Verification Code"
},
"body": {
"ko": "<h1>결제 본인 확인</h1><p>아래 인증 코드를 입력해 주세요.</p><p>{code}</p>",
"en": "<h1>Checkout Verification</h1><p>Please enter the code below.</p><p>{code}</p>"
},
"is_active": true,
"is_default": false,
"user_overrides": [
"subject",
"body"
],
"updated_by": null,
"created_at": "2026-07-08 10:43:32",
"updated_at": "2026-07-08 12:14:36",
"abilities": {
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 수정 중 예외 발생 (`IDV 메시지 템플릿 수정에 실패했습니다.`) |
<!-- @generated:end -->
@@ -1325,7 +1581,7 @@ HTTP/1.1 200
```json
{
"success": true,
"message": "messages.success",
"message": "성공적으로 처리되었습니다.",
"data": {
"data": [
{
@@ -1458,19 +1714,72 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (생성된 정책을 `PolicyResource` 로 직렬화). 성공 시 HTTP 201._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `25` | 기본 키 (내부 식별자) |
| key | string | `admin.custom_action` | 정책 식별자 (고유) |
| scope | string | `route` | 정책 적용 범위 (route / hook / custom — IdentityPolicyScope) |
| target | string | `api.me.password.update` | 매칭 대상 (라우트명/URI 패턴, 훅 이름, 또는 custom key) |
| purpose | string | `sensitive_action` | 이 정책이 요구하는 인증 목적 |
| provider_id | null | `null` | 강제할 프로바이더 ID (미지정 시 null → 목적 기본 프로바이더 사용) |
| grace_minutes | integer | `30` | 재인증 유예 시간(분) — 0=매번 요구 |
| enabled | boolean | `true` | 정책 사용 여부 |
| priority | integer | `100` | 정책 우선순위 (작을수록 우선) |
| conditions | object | `{"changed_fields":["email"]}` | 추가 매칭 조건 JSON (미지정 시 null) |
| source_type | string | `admin` | 정책 출처 — 이 엔드포인트로 생성한 정책은 항상 `admin` |
| source_identifier | string | `null` | 출처 식별자 (요청에서 전달한 값, 미전달 시 null) |
| applies_to | string | `both` | 적용 대상 사용자 (self / admin / both) |
| fail_mode | string | `block` | 실패 시 동작 (block: HTTP 428 차단 / log_only: 감사 로그만) |
| user_overrides | array | `[]` | 운영자가 선언 기본값에서 재정의한 필드 목록 (admin 정책은 빈 배열) |
| created_at | string | `2026-07-08 10:44:35` | 생성 일시 |
| updated_at | string | `2026-07-08 10:44:35` | 최종 수정 일시 |
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "생성되었습니다.",
"data": {
"id": 25,
"key": "admin.custom_action",
"scope": "route",
"target": "api.me.password.update",
"purpose": "sensitive_action",
"provider_id": null,
"grace_minutes": 30,
"enabled": true,
"priority": 100,
"conditions": null,
"source_type": "admin",
"source_identifier": null,
"applies_to": "both",
"fail_mode": "block",
"user_overrides": [],
"created_at": "2026-07-08 10:44:35",
"updated_at": "2026-07-08 10:44:35",
"abilities": {
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 403 | Forbidden | 요구 권한(`core.admin.identity.policies.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지 — `key` 중복, `scope`/`applies_to`/`fail_mode` 허용값 위반 등) |
<!-- @generated:end -->
@@ -1502,20 +1811,30 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — 컨트롤러가 `$this->success('messages.deleted')` 를 인자 1개로 호출)._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "삭제되었습니다."
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.policies.update`)이 없는 경우, 또는 대상 정책의 `source_type` 이 `admin` 이 아닌 선언형 정책(core/module/plugin)인 경우 (`messages.cannot_delete_system_resource`) |
| 404 | Not Found | 해당 ID 의 정책이 없는 경우 (`messages.not_found`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 삭제 처리 실패 (`messages.failed`) |
<!-- @generated:end -->
@@ -1575,20 +1894,77 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (수정 후 `refresh()` 한 정책을 `PolicyResource` 로 직렬화 — 목록 항목과 동일 구조)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `11` | 기본 키 (내부 식별자) |
| key | string | `sirsoft-board.report.create` | 정책 식별자 (선언형 정책은 변경 불가) |
| scope | string | `hook` | 정책 적용 범위 (선언형 정책은 변경 불가) |
| target | string | `sirsoft-board.report.before_create` | 매칭 대상 (선언형 정책은 변경 불가) |
| purpose | string | `sensitive_action` | 이 정책이 요구하는 인증 목적 (수정 반영) |
| provider_id | null | `null` | 강제할 프로바이더 ID (수정 반영) |
| grace_minutes | integer | `30` | 재인증 유예 시간(분) (수정 반영) |
| enabled | boolean | `true` | 정책 사용 여부 (수정 반영) |
| priority | integer | `100` | 정책 우선순위 (수정 반영) |
| conditions | object | `null` | 추가 매칭 조건 JSON (수정 반영) |
| source_type | string | `module` | 정책 출처 (core/module/plugin/admin) |
| source_identifier | string | `sirsoft-board` | 출처 식별자 |
| applies_to | string | `self` | 적용 대상 사용자 (수정 반영) |
| fail_mode | string | `block` | 실패 시 동작 (수정 반영) |
| user_overrides | array | `["enabled","grace_minutes"]` | 이번 수정으로 append 된 운영자 재정의 필드 목록 (시더 재실행 시 보존) |
| created_at | string | `2026-07-08 10:44:35` | 생성 일시 |
| updated_at | string | `2026-07-08 12:14:36` | 최종 수정 일시 |
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "수정되었습니다.",
"data": {
"id": 11,
"key": "sirsoft-board.report.create",
"scope": "hook",
"target": "sirsoft-board.report.before_create",
"purpose": "sensitive_action",
"provider_id": null,
"grace_minutes": 30,
"enabled": true,
"priority": 100,
"conditions": null,
"source_type": "module",
"source_identifier": "sirsoft-board",
"applies_to": "self",
"fail_mode": "block",
"user_overrides": [
"enabled",
"grace_minutes"
],
"created_at": "2026-07-08 10:44:35",
"updated_at": "2026-07-08 12:14:36",
"abilities": {
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 403 | Forbidden | 요구 권한(`core.admin.identity.policies.update`)이 없는 경우 |
| 404 | Not Found | 해당 ID 의 정책이 없는 경우 (`messages.not_found`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우, 또는 선언형 정책에 화이트리스트(`enabled`/`grace_minutes`/`provider_id`/`fail_mode`/`conditions`/`purpose`/`applies_to`/`priority`) 밖 필드만 전달되어 적용할 값이 남지 않은 경우 (`validation.nothing_to_update`) |
| 500 | Internal Server Error | 수정 처리 실패 (`messages.failed`) |
<!-- @generated:end -->
@@ -1626,20 +2002,73 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (복원 후 `fresh()` 한 정책을 `PolicyResource` 로 직렬화 — 목록 항목과 동일 구조)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `11` | 기본 키 (내부 식별자) |
| key | string | `sirsoft-board.report.create` | 정책 식별자 |
| scope | string | `hook` | 정책 적용 범위 (route / hook / custom) |
| target | string | `sirsoft-board.report.before_create` | 매칭 대상 |
| purpose | string | `sensitive_action` | 이 정책이 요구하는 인증 목적 |
| provider_id | null | `null` | 강제할 프로바이더 ID |
| grace_minutes | integer | `0` | 재인증 유예 시간(분) — 요청한 `field` 가 이 필드였다면 선언 기본값으로 복원된 값 |
| enabled | boolean | `false` | 정책 사용 여부 — 요청한 `field` 가 이 필드였다면 선언 기본값으로 복원된 값 |
| priority | integer | `100` | 정책 우선순위 |
| conditions | object | `null` | 추가 매칭 조건 JSON |
| source_type | string | `module` | 정책 출처 (admin 이면 이 엔드포인트는 403) |
| source_identifier | string | `sirsoft-board` | 출처 식별자 |
| applies_to | string | `self` | 적용 대상 사용자 |
| fail_mode | string | `block` | 실패 시 동작 |
| user_overrides | array | `[]` | 남아 있는 운영자 재정의 필드 목록 (요청한 `field` 가 제거된 상태) |
| created_at | string | `2026-07-08 10:44:35` | 생성 일시 |
| updated_at | string | `2026-07-08 12:20:11` | 최종 수정 일시 |
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"id": 11,
"key": "sirsoft-board.report.create",
"scope": "hook",
"target": "sirsoft-board.report.before_create",
"purpose": "sensitive_action",
"provider_id": null,
"grace_minutes": 0,
"enabled": false,
"priority": 100,
"conditions": null,
"source_type": "module",
"source_identifier": "sirsoft-board",
"applies_to": "self",
"fail_mode": "block",
"user_overrides": [],
"created_at": "2026-07-08 10:44:35",
"updated_at": "2026-07-08 12:20:11",
"abilities": {
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 403 | Forbidden | 요구 권한(`core.admin.identity.policies.update`)이 없는 경우, 또는 대상 정책의 `source_type` 이 `admin` 이라 선언 기본값이 존재하지 않는 경우 (`관리자가 직접 생성한 정책에는 선언 기본값이 없습니다.`) |
| 404 | Not Found | 해당 ID 의 정책이 없는 경우 (`messages.not_found`) |
| 422 | Unprocessable Entity | `field` 가 허용값 밖이거나 복원에 실패한 경우 (`선언 기본값 복원에 실패했습니다. 필드가 유효한지 확인하세요.`) |
<!-- @generated:end -->
@@ -1687,7 +2116,7 @@ HTTP/1.1 200
```json
{
"success": true,
"message": "messages.success",
"message": "성공적으로 처리되었습니다.",
"data": [
{
"id": "g7:core.mail",
@@ -1831,20 +2260,49 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (성공 + `return` 쿼리가 없을 때만 JSON 을 반환합니다. `return` 이 있고 same-origin 이면 302 redirect 이므로 본문이 없습니다)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| challenge_id | string | `00484973-8cd3-4a1d-85f2-78361feb6f0d` | 검증이 완료된 challenge 의 식별자 |
| provider_id | string | `inicis` | 이 콜백을 처리한 프로바이더 식별자 |
| verified_at | string | `2026-05-12T18:14:19+00:00` | 검증 완료 일시 (ISO 8601, 미검증이면 null) |
| verification_token | string | `eyJ0eXAiOiJKV1QiLCJhbGciOi…` | 후속 민감 작업 요청에 제출할 본인인증 토큰 (`claims.verification_token`, 없으면 빈 문자열) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "본인 확인이 완료되었습니다.",
"data": {
"challenge_id": "00484973-8cd3-4a1d-85f2-78361feb6f0d",
"provider_id": "inicis",
"verified_at": "2026-05-12T18:14:19+00:00",
"verification_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}
}
```
`return` 쿼리(same-origin) 가 있으면 JSON 대신 302 redirect 입니다.
```http
HTTP/1.1 302
Location: /checkout?verification_token=eyJ0eXAiOiJKV1Qi...&challenge_id=00484973-8cd3-4a1d-85f2-78361feb6f0d
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 302 | Found (실패 redirect) | 검증 실패 + `return` 쿼리(same-origin) 존재 시 `{return}?identity_error={failure_code}` 로 리다이렉트 |
| 401 | Unauthenticated | Bearer 토큰을 보냈으나 만료/무효인 경우 (비회원은 헤더 생략 가능) |
| 404 | Not Found | `providerId` 에 해당하는 프로바이더가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터 검증 실패, 또는 검증 실패 + `return` 쿼리 없음 — 본문의 `error.failure_code` 에 실패 사유 코드 (`CHALLENGE_NOT_FOUND`/`EXPIRED`/`INVALID_CODE` 등, `identity.errors.*` 메시지 동반) |
<!-- @generated:end -->
@@ -1863,8 +2321,8 @@ Content-Type: application/json
| --- | --- | --- | --- | --- | --- |
| purpose | body | string | 예 | max 64 | 인증 목적 (signup/password_reset/self_update/sensitive_action/login 또는 모듈 정의 목적) |
| target | body | array | 아니오 | — | 비로그인 게스트의 인증 대상 (target.email 또는 target.phone — 로그인 사용자는 본인으로 자동 설정) |
| target.email | body | email | 아니오 | max 255 | <!-- TODO: 용도 --> |
| target.phone | body | string | 아니오 | max 32 | <!-- TODO: 용도 --> |
| target.email | body | email | 아니오 | max 255 | 이메일 주소 |
| target.phone | body | string | 아니오 | max 32 | 전화번호 |
| provider_id | body | string | 아니오 | max 64 | provider 식별자 |
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity.request_validation_rules`).
@@ -1891,19 +2349,51 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-429 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ChallengeResource` 로 직렬화한 VerificationChallenge DTO — 민감 metadata(code_hash 등) 제외). 성공 시 HTTP 201._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | string | `00484973-8cd3-4a1d-85f2-78361feb6f0d` | 생성된 challenge 의 식별자 (이후 verify/cancel/show 에 사용) |
| provider_id | string | `g7:core.mail` | 이 challenge 를 처리하는 프로바이더 식별자 |
| purpose | string | `signup` | 인증 목적 |
| channel | string | `email` | 실제 발송에 사용된 전송 채널 |
| render_hint | string | `text_code` | 프론트 렌더 방식 힌트 (text_code / link / external_redirect) |
| redirect_url | string | `null` | external_redirect 흐름에서 사용자를 보낼 외부 인증 페이지 URL (그 외 흐름은 null) |
| expires_at | string | `2026-05-12T18:14:19+00:00` | 만료 일시 (ISO 8601) |
| public_payload | array | `[]` | 프론트 렌더에 필요한 공개 안전 페이로드 (프로바이더별 UI 힌트, 없으면 빈 배열) |
| max_attempts | integer | `5` | 허용되는 최대 검증 시도 횟수 |
**응답 예시**
<!-- 실측 제외: http-429 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "본인인증 코드를 발송했습니다.",
"data": {
"id": "00484973-8cd3-4a1d-85f2-78361feb6f0d",
"provider_id": "g7:core.mail",
"purpose": "signup",
"channel": "email",
"render_hint": "text_code",
"redirect_url": null,
"expires_at": "2026-05-12T18:14:19+00:00",
"public_payload": [],
"max_attempts": 5
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 401 | Unauthenticated | Bearer 토큰을 보냈으나 만료/무효인 경우 (비회원은 헤더 생략 가능) |
| 403 | Forbidden | 요구 권한(`core.identity.request`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터 검증 실패, 또는 비로그인 상태에서 `target.email`·`target.phone` 이 모두 비어 있는 경우 (`인증 대상(이메일·전화번호)이 필요합니다.`), 선택한 프로바이더가 해당 목적을 지원하지 않는 경우(`선택된 프로바이더는 이 목적을 지원하지 않습니다.`), 프로바이더 사용 불가(`본인인증 프로바이더를 사용할 수 없습니다.`) |
<!-- @generated:end -->
@@ -1957,7 +2447,7 @@ HTTP/1.1 200
```json
{
"success": true,
"message": "messages.success",
"message": "성공적으로 처리되었습니다.",
"data": {
"id": "006209fd-1b6c-40aa-a38f-9b907f1a4bca",
"status": "requested",
@@ -2008,19 +2498,28 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-429 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — 컨트롤러가 `$this->success('identity.messages.challenge_cancelled')` 를 인자 1개로 호출)._
**응답 예시**
<!-- 실측 제외: http-429 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "본인인증 요청이 취소되었습니다."
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 401 | Unauthenticated | Bearer 토큰을 보냈으나 만료/무효인 경우 (비회원은 헤더 생략 가능) |
| 403 | Forbidden | 요구 권한(`core.identity.cancel`)이 없는 경우, 또는 scope=self 가드가 본인 challenge 가 아니라고 판정한 경우 |
| 404 | Not Found | path 의 challenge 를 찾을 수 없거나 취소 처리에 실패한 경우 (`유효하지 않은 인증 요청입니다.`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
<!-- @generated:end -->
@@ -2061,20 +2560,42 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-429 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (컨트롤러가 VerificationResult DTO 에서 직접 조립)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| challenge_id | string | `00484973-8cd3-4a1d-85f2-78361feb6f0d` | 검증이 완료된 challenge 의 식별자 |
| provider_id | string | `g7:core.mail` | 이 challenge 를 처리한 프로바이더 식별자 |
| verified_at | string | `2026-05-12T18:14:19+00:00` | 검증 완료 일시 (ISO 8601, 없으면 null) |
| verification_token | string | `eyJ0eXAiOiJKV1QiLCJhbGciOi…` | 후속 민감 작업 요청에 제출할 본인인증 토큰 (`claims.verification_token`, 없으면 null) |
**응답 예시**
<!-- 실측 제외: http-429 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "본인 확인이 완료되었습니다.",
"data": {
"challenge_id": "00484973-8cd3-4a1d-85f2-78361feb6f0d",
"provider_id": "g7:core.mail",
"verified_at": "2026-05-12T18:14:19+00:00",
"verification_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 401 | Unauthenticated | Bearer 토큰을 보냈으나 만료/무효인 경우 (비회원은 헤더 생략 가능) |
| 403 | Forbidden | 요구 권한(`core.identity.verify`)이 없는 경우, 또는 scope=self 가드가 본인 challenge 가 아니라고 판정한 경우 |
| 404 | Not Found | path 파라미터에 해당하는 challenge 가 없는 경우 |
| 422 | Unprocessable Entity | 검증 실패 — 응답 `error` 에 `failure_code`(예: `INVALID_CODE`/`EXPIRED`/`MAX_ATTEMPTS`) 와 서버 기준 `attempts`/`max_attempts` 를 함께 반환. 메시지는 `identity.errors.*` (`인증 코드가 올바르지 않습니다.` / `인증 시간이 만료되었습니다. 다시 시도해주세요.` / `시도 횟수를 초과했습니다. 다시 요청해주세요.` 등) |
<!-- @generated:end -->
@@ -2105,19 +2626,58 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (매칭·활성 정책이 없으면 `data` 는 `null`). 컨트롤러가 UI 힌트용 최소 필드만 직접 조립하며 민감 필드는 노출하지 않습니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| policy_key | string | `sirsoft-board.report.create` | 매칭된 정책의 식별자 |
| scope | string | `hook` | 정책 적용 범위 (route / hook / custom) |
| target | string | `sirsoft-board.report.before_create` | 매칭 대상 |
| purpose | string | `sensitive_action` | 이 정책이 요구하는 인증 목적 |
| provider_id | null | `null` | 강제할 프로바이더 ID (미지정 시 null) |
| grace_minutes | integer | `30` | 재인증 유예 시간(분) — 0=매번 요구 |
| applies_to | string | `self` | 적용 대상 사용자 (self / admin / both) |
| fail_mode | string | `block` | 실패 시 동작 (block: HTTP 428 차단 / log_only: 감사 로그만) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"policy_key": "sirsoft-board.report.create",
"scope": "hook",
"target": "sirsoft-board.report.before_create",
"purpose": "sensitive_action",
"provider_id": null,
"grace_minutes": 30,
"applies_to": "self",
"fail_mode": "block"
}
}
```
매칭 정책이 없거나 `enabled=false` 인 경우:
```json
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 401 | Unauthenticated | Bearer 토큰을 보냈으나 만료/무효인 경우 (비회원은 헤더 생략 가능) |
| 422 | Unprocessable Entity | 필수 쿼리 `scope`/`target` 누락 또는 길이 초과 (`error.errors` 에 필드별 메시지) |
<!-- @generated:end -->
@@ -2163,7 +2723,7 @@ HTTP/1.1 200
```json
{
"success": true,
"message": "messages.success",
"message": "성공적으로 처리되었습니다.",
"data": [
{
"id": "g7:core.mail",
@@ -2251,7 +2811,7 @@ HTTP/1.1 200
```json
{
"success": true,
"message": "messages.success",
"message": "성공적으로 처리되었습니다.",
"data": [
{
"id": "signup",
+766 -24
View File
@@ -248,11 +248,41 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`LanguagePackService::bulkActivate()` 반환값)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| succeeded | array | `[3, 5]` | 활성화에 성공한 언어팩 ID 목록 |
| failed | array | `[{"id": 7, "reason": "not_found"}]` | 활성화에 실패한 항목 목록 (요소: `id`, `reason`) |
| failed[].id | integer\|string | `7` | 실패한 항목의 요청 ID (요청 `ids` 원본 값) |
| failed[].reason | string | `not_found` | 실패 사유. 대상 미존재 시 `not_found`, 그 외에는 발생한 예외 메시지 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "language_packs.bulk_activate_success",
"data": {
"succeeded": [
3,
5
],
"failed": [
{
"id": 7,
"reason": "not_found"
}
]
}
}
```
> `message` 는 `language_packs.bulk_activate_success` 키로 응답하지만, 현재 `lang/ko/language_packs.php` 에 해당 키가 정의되어 있지 않아 번역되지 않은 키 문자열이 그대로 내려갑니다.
**에러 응답**
@@ -288,11 +318,51 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`LanguagePackService::checkUpdates()` 반환값)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| checked | integer | `3` | 실제로 검사한 언어팩 수 (GitHub URL 해석 가능하거나 번들 매니페스트가 있는 팩만 대상) |
| updates | integer | `1` | 검사 결과 업데이트가 있는 언어팩 수 |
| details | array | `[{"identifier": "g7-core-ja", "current": "1.0.0", "latest": "1.1.0", "has_update": true, "error": null}]` | 팩별 검사 결과 목록 |
| details[].identifier | string | `g7-core-ja` | 검사 대상 언어팩 식별자 |
| details[].current | string | `1.0.0` | 현재 설치된 버전 |
| details[].latest | string\|null | `1.1.0` | 감지된 최신 버전 (GitHub 릴리스 태그 → 실패 시 번들 매니페스트 폴백, 둘 다 없으면 null) |
| details[].has_update | boolean | `true` | `latest > current` 여부 |
| details[].error | string\|null | `null` | GitHub 조회 중 발생한 오류 메시지 (성공 시 null) |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "업데이트 확인을 완료했습니다.",
"data": {
"checked": 3,
"updates": 1,
"details": [
{
"identifier": "g7-core-ja",
"current": "1.0.0",
"latest": "1.1.0",
"has_update": true,
"error": null
},
{
"identifier": "apidoc-sample-lang",
"current": "1.0.0",
"latest": "1.0.0",
"has_update": false,
"error": null
}
]
}
}
```
**에러 응답**
@@ -301,6 +371,7 @@ Authorization: Bearer {YOUR_TOKEN}
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.language_packs.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 업데이트 확인 중 예외 발생 (`업데이트 확인에 실패했습니다: :error` — `language_packs.check_updates_failed`) |
<!-- @generated:end -->
@@ -337,11 +408,93 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (설치된 언어팩 = `LanguagePackResource::toArray()`, 상태코드 201)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| identifier | string | `g7-core-ja` | 언어팩 고유 식별자 ({vendor}-{scope}-{target?}-{locale}) |
| vendor | string | `g7` | 언어팩 제작자 식별자 |
| scope | string | `core` | 적용 대상 분류 (core/module/plugin/template) |
| target_identifier | string\|null | `null` | 대상 확장 식별자 (scope=core 일 때 null) |
| locale | string | `ja` | IETF BCP-47 locale 태그 |
| locale_name | string | `Japanese` | 영문 언어명 |
| locale_native_name | string | `日本語` | 원어 언어명 |
| text_direction | string | `ltr` | 텍스트 방향 (ltr/rtl) |
| version | string | `1.0.0` | 설치된 언어팩 버전 |
| latest_version | string\|null | `1.0.0` | 감지된 최신 배포 버전 |
| target_version_constraint | string\|null | `null` | 대상 확장 버전 제약 (semver) |
| target_version_mismatch | boolean | `false` | 대상 버전 불일치 경고 플래그 |
| name | string\|null | `일본어 언어팩` | manifest `name` 의 현재 로케일 해석값 |
| license | string\|null | `MIT` | 라이선스 |
| description | string\|null | `코어 일본어 번역` | 언어팩 설명 (다국어) |
| status | string | `active` | 언어팩 상태 (auto_activate=true 면 active, 아니면 installed) |
| is_protected | boolean | `false` | 보호(protected) 팩 여부 |
| source_type | string | `bundled` | 설치 소스 유형 (zip/github/url/bundled/bundled_with_extension/built_in) |
| origin | string\|null | `bundled` | source_type 에서 파생된 출처 값 (`LanguagePackOrigin`) |
| source_url | string\|null | `null` | 설치 소스 URL 또는 경로 |
| github_url | string\|null | `null` | GitHub 저장소 URL (manifest 파생) |
| github_changelog_url | string\|null | `null` | GitHub 변경 이력(CHANGELOG) URL (manifest 파생) |
| bundled_identifier | string\|null | `null` | 대응하는 번들 확장 식별자 (번들 원본 매칭용) |
| install_blocked_reason | string\|null | `null` | 설치가 차단된 사유 (차단 없으면 null) |
| target_name | string\|null | `null` | 대상 확장의 표시 이름 (scope+target_identifier 로 해석) |
| installed_at | string\|null | `2026-07-05 10:41:24` | 설치 일시 (사용자 타임존) |
| activated_at | string\|null | `2026-07-05 10:41:24` | 활성화 일시 (사용자 타임존) |
| created_at | string\|null | `2026-07-05 10:41:24` | 생성 일시 |
| updated_at | string\|null | `2026-07-05 10:41:24` | 최종 수정 일시 |
| has_update | boolean | `false` | `latest_version > version` 여부 |
| abilities | object | `{"can_activate":true,"can_deactivate":true,"can_uninstall":true}` | 현재 사용자가 이 언어팩에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "언어팩을 설치했습니다.",
"data": {
"id": 1,
"identifier": "g7-core-ja",
"vendor": "g7",
"scope": "core",
"target_identifier": null,
"locale": "ja",
"locale_name": "Japanese",
"locale_native_name": "日本語",
"text_direction": "ltr",
"version": "1.0.0",
"latest_version": "1.0.0",
"target_version_constraint": null,
"target_version_mismatch": false,
"name": "일본어 언어팩",
"license": "MIT",
"description": "코어 일본어 번역",
"status": "active",
"is_protected": false,
"source_type": "bundled",
"origin": "bundled",
"source_url": null,
"github_url": null,
"github_changelog_url": null,
"bundled_identifier": null,
"install_blocked_reason": null,
"target_name": null,
"installed_at": "2026-07-05 10:41:24",
"activated_at": "2026-07-05 10:41:24",
"created_at": "2026-07-05 10:41:24",
"updated_at": "2026-07-05 10:41:24",
"has_update": false,
"abilities": {
"can_activate": true,
"can_deactivate": true,
"can_uninstall": true
}
}
}
```
**에러 응답**
@@ -392,11 +545,95 @@ Content-Disposition: form-data; name="auto_activate"
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (설치된 언어팩 = `LanguagePackResource::toArray()`, 상태코드 201). 필드 구성은 [install-from-bundled](#post-apiadminlanguage-packsinstall-from-bundled) 응답과 동일합니다 — `source_type` 은 `zip`._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `2` | 기본 키 (내부 식별자) |
| identifier | string | `acme-core-de` | 언어팩 고유 식별자 |
| vendor | string | `acme` | 언어팩 제작자 식별자 |
| scope | string | `core` | 적용 대상 분류 (core/module/plugin/template) |
| target_identifier | string\|null | `null` | 대상 확장 식별자 (scope=core 일 때 null) |
| locale | string | `de` | IETF BCP-47 locale 태그 |
| locale_name | string | `German` | 영문 언어명 |
| locale_native_name | string | `Deutsch` | 원어 언어명 |
| text_direction | string | `ltr` | 텍스트 방향 (ltr/rtl) |
| version | string | `1.0.0` | 설치된 언어팩 버전 |
| latest_version | string\|null | `null` | 감지된 최신 배포 버전 |
| target_version_constraint | string\|null | `null` | 대상 확장 버전 제약 (semver) |
| target_version_mismatch | boolean | `false` | 대상 버전 불일치 경고 플래그 |
| name | string\|null | `독일어 언어팩` | manifest `name` 의 현재 로케일 해석값 |
| license | string\|null | `MIT` | 라이선스 |
| description | string\|null | `—` | 언어팩 설명 (다국어) |
| status | string | `installed` | 언어팩 상태 (auto_activate=true 면 active) |
| is_protected | boolean | `false` | 보호(protected) 팩 여부 |
| source_type | string | `zip` | 설치 소스 유형 (파일 업로드 설치이므로 zip) |
| origin | string\|null | `zip` | source_type 에서 파생된 출처 값 |
| source_url | string\|null | `null` | 설치 소스 URL 또는 경로 |
| github_url | string\|null | `null` | GitHub 저장소 URL (manifest 파생) |
| github_changelog_url | string\|null | `null` | GitHub 변경 이력(CHANGELOG) URL (manifest 파생) |
| bundled_identifier | string\|null | `null` | 대응하는 번들 확장 식별자 |
| install_blocked_reason | string\|null | `null` | 설치가 차단된 사유 (차단 없으면 null) |
| target_name | string\|null | `null` | 대상 확장의 표시 이름 |
| installed_at | string\|null | `2026-07-05 10:41:24` | 설치 일시 (사용자 타임존) |
| activated_at | string\|null | `null` | 활성화 일시 (미활성 시 null) |
| created_at | string\|null | `2026-07-05 10:41:24` | 생성 일시 |
| updated_at | string\|null | `2026-07-05 10:41:24` | 최종 수정 일시 |
| has_update | boolean | `false` | `latest_version > version` 여부 |
| abilities | object | `{"can_activate":true,"can_deactivate":true,"can_uninstall":true}` | 현재 사용자가 이 언어팩에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "언어팩을 설치했습니다.",
"data": {
"id": 2,
"identifier": "acme-core-de",
"vendor": "acme",
"scope": "core",
"target_identifier": null,
"locale": "de",
"locale_name": "German",
"locale_native_name": "Deutsch",
"text_direction": "ltr",
"version": "1.0.0",
"latest_version": null,
"target_version_constraint": null,
"target_version_mismatch": false,
"name": "독일어 언어팩",
"license": "MIT",
"description": null,
"status": "installed",
"is_protected": false,
"source_type": "zip",
"origin": "zip",
"source_url": null,
"github_url": null,
"github_changelog_url": null,
"bundled_identifier": null,
"install_blocked_reason": null,
"target_name": null,
"installed_at": "2026-07-05 10:41:24",
"activated_at": null,
"created_at": "2026-07-05 10:41:24",
"updated_at": "2026-07-05 10:41:24",
"has_update": false,
"abilities": {
"can_activate": true,
"can_deactivate": true,
"can_uninstall": true
}
}
}
```
> manifest 검증 실패 시 422 (`language-pack.json 검증에 실패했습니다.`), 그 외 설치 실패 시 500 (`언어팩 설치에 실패했습니다: :error`) 으로 응답합니다.
**에러 응답**
@@ -441,11 +678,93 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (설치된 언어팩 = `LanguagePackResource::toArray()`, 상태코드 201). 필드 구성은 install-from-bundled 응답과 동일하며 `source_type` 이 `github`, `source_url`/`github_url` 에 저장소 URL 이 들어갑니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `3` | 기본 키 (내부 식별자) |
| identifier | string | `acme-core-es` | 언어팩 고유 식별자 |
| vendor | string | `acme` | 언어팩 제작자 식별자 |
| scope | string | `core` | 적용 대상 분류 (core/module/plugin/template) |
| target_identifier | string\|null | `null` | 대상 확장 식별자 (scope=core 일 때 null) |
| locale | string | `es` | IETF BCP-47 locale 태그 |
| locale_name | string | `Spanish` | 영문 언어명 |
| locale_native_name | string | `Español` | 원어 언어명 |
| text_direction | string | `ltr` | 텍스트 방향 (ltr/rtl) |
| version | string | `1.0.0` | 설치된 언어팩 버전 (GitHub 최신 릴리스 태그 기준) |
| latest_version | string\|null | `1.0.0` | 감지된 최신 배포 버전 |
| target_version_constraint | string\|null | `null` | 대상 확장 버전 제약 (semver) |
| target_version_mismatch | boolean | `false` | 대상 버전 불일치 경고 플래그 |
| name | string\|null | `스페인어 언어팩` | manifest `name` 의 현재 로케일 해석값 |
| license | string\|null | `MIT` | 라이선스 |
| description | string\|null | `—` | 언어팩 설명 (다국어) |
| status | string | `active` | 언어팩 상태 (auto_activate=true 면 active, 아니면 installed) |
| is_protected | boolean | `false` | 보호(protected) 팩 여부 |
| source_type | string | `github` | 설치 소스 유형 |
| origin | string\|null | `github` | source_type 에서 파생된 출처 값 |
| source_url | string\|null | `https://github.com/acme/g7-core-es` | 설치에 사용한 GitHub 저장소 URL |
| github_url | string\|null | `https://github.com/acme/g7-core-es` | GitHub 저장소 URL (manifest 파생) |
| github_changelog_url | string\|null | `null` | GitHub 변경 이력(CHANGELOG) URL (manifest 파생) |
| bundled_identifier | string\|null | `null` | 대응하는 번들 확장 식별자 |
| install_blocked_reason | string\|null | `null` | 설치가 차단된 사유 (차단 없으면 null) |
| target_name | string\|null | `null` | 대상 확장의 표시 이름 |
| installed_at | string\|null | `2026-07-05 10:41:24` | 설치 일시 (사용자 타임존) |
| activated_at | string\|null | `2026-07-05 10:41:24` | 활성화 일시 |
| created_at | string\|null | `2026-07-05 10:41:24` | 생성 일시 |
| updated_at | string\|null | `2026-07-05 10:41:24` | 최종 수정 일시 |
| has_update | boolean | `false` | `latest_version > version` 여부 |
| abilities | object | `{"can_activate":true,"can_deactivate":true,"can_uninstall":true}` | 현재 사용자가 이 언어팩에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "언어팩을 설치했습니다.",
"data": {
"id": 3,
"identifier": "acme-core-es",
"vendor": "acme",
"scope": "core",
"target_identifier": null,
"locale": "es",
"locale_name": "Spanish",
"locale_native_name": "Español",
"text_direction": "ltr",
"version": "1.0.0",
"latest_version": "1.0.0",
"target_version_constraint": null,
"target_version_mismatch": false,
"name": "스페인어 언어팩",
"license": "MIT",
"description": null,
"status": "active",
"is_protected": false,
"source_type": "github",
"origin": "github",
"source_url": "https://github.com/acme/g7-core-es",
"github_url": "https://github.com/acme/g7-core-es",
"github_changelog_url": null,
"bundled_identifier": null,
"install_blocked_reason": null,
"target_name": null,
"installed_at": "2026-07-05 10:41:24",
"activated_at": "2026-07-05 10:41:24",
"created_at": "2026-07-05 10:41:24",
"updated_at": "2026-07-05 10:41:24",
"has_update": false,
"abilities": {
"can_activate": true,
"can_deactivate": true,
"can_uninstall": true
}
}
}
```
**에러 응답**
@@ -494,11 +813,95 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (설치된 언어팩 = `LanguagePackResource::toArray()`, 상태코드 201). 필드 구성은 install-from-bundled 응답과 동일하며 `source_type` 이 `url`, `source_url` 에 다운로드 URL 이 들어갑니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `4` | 기본 키 (내부 식별자) |
| identifier | string | `acme-core-it` | 언어팩 고유 식별자 |
| vendor | string | `acme` | 언어팩 제작자 식별자 |
| scope | string | `core` | 적용 대상 분류 (core/module/plugin/template) |
| target_identifier | string\|null | `null` | 대상 확장 식별자 (scope=core 일 때 null) |
| locale | string | `it` | IETF BCP-47 locale 태그 |
| locale_name | string | `Italian` | 영문 언어명 |
| locale_native_name | string | `Italiano` | 원어 언어명 |
| text_direction | string | `ltr` | 텍스트 방향 (ltr/rtl) |
| version | string | `1.0.0` | 설치된 언어팩 버전 |
| latest_version | string\|null | `null` | 감지된 최신 배포 버전 |
| target_version_constraint | string\|null | `null` | 대상 확장 버전 제약 (semver) |
| target_version_mismatch | boolean | `false` | 대상 버전 불일치 경고 플래그 |
| name | string\|null | `이탈리아어 언어팩` | manifest `name` 의 현재 로케일 해석값 |
| license | string\|null | `MIT` | 라이선스 |
| description | string\|null | `—` | 언어팩 설명 (다국어) |
| status | string | `active` | 언어팩 상태 (auto_activate=true 면 active, 아니면 installed) |
| is_protected | boolean | `false` | 보호(protected) 팩 여부 |
| source_type | string | `url` | 설치 소스 유형 |
| origin | string\|null | `url` | source_type 에서 파생된 출처 값 |
| source_url | string\|null | `https://cdn.example.com/acme-core-it.zip` | 다운로드에 사용한 URL |
| github_url | string\|null | `null` | GitHub 저장소 URL (manifest 파생) |
| github_changelog_url | string\|null | `null` | GitHub 변경 이력(CHANGELOG) URL (manifest 파생) |
| bundled_identifier | string\|null | `null` | 대응하는 번들 확장 식별자 |
| install_blocked_reason | string\|null | `null` | 설치가 차단된 사유 (차단 없으면 null) |
| target_name | string\|null | `null` | 대상 확장의 표시 이름 |
| installed_at | string\|null | `2026-07-05 10:41:24` | 설치 일시 (사용자 타임존) |
| activated_at | string\|null | `2026-07-05 10:41:24` | 활성화 일시 |
| created_at | string\|null | `2026-07-05 10:41:24` | 생성 일시 |
| updated_at | string\|null | `2026-07-05 10:41:24` | 최종 수정 일시 |
| has_update | boolean | `false` | `latest_version > version` 여부 |
| abilities | object | `{"can_activate":true,"can_deactivate":true,"can_uninstall":true}` | 현재 사용자가 이 언어팩에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "언어팩을 설치했습니다.",
"data": {
"id": 4,
"identifier": "acme-core-it",
"vendor": "acme",
"scope": "core",
"target_identifier": null,
"locale": "it",
"locale_name": "Italian",
"locale_native_name": "Italiano",
"text_direction": "ltr",
"version": "1.0.0",
"latest_version": null,
"target_version_constraint": null,
"target_version_mismatch": false,
"name": "이탈리아어 언어팩",
"license": "MIT",
"description": null,
"status": "active",
"is_protected": false,
"source_type": "url",
"origin": "url",
"source_url": "https://cdn.example.com/acme-core-it.zip",
"github_url": null,
"github_changelog_url": null,
"bundled_identifier": null,
"install_blocked_reason": null,
"target_name": null,
"installed_at": "2026-07-05 10:41:24",
"activated_at": "2026-07-05 10:41:24",
"created_at": "2026-07-05 10:41:24",
"updated_at": "2026-07-05 10:41:24",
"has_update": false,
"abilities": {
"can_activate": true,
"can_deactivate": true,
"can_uninstall": true
}
}
}
```
> `checksum` 불일치 시 500 (`체크섬이 일치하지 않습니다.`), manifest 검증 실패 시 422 로 응답합니다.
**에러 응답**
@@ -544,11 +947,51 @@ Content-Type: application/octet-stream
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`LanguagePackService::previewManifest()` 반환값)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| manifest | object | `{"identifier":"acme-core-de","locale":"de","version":"1.0.0"}` | ZIP 안 `language-pack.json` 원본 전체 (파싱된 객체) |
| validation | object | `{"errors":[],"is_valid":true,...}` | manifest 검증 결과 요약 |
| validation.errors | array | `[]` | 검증 실패 메시지 목록 (통과 시 빈 배열) |
| validation.is_valid | boolean | `true` | 검증 통과 여부 (`errors` 가 비어 있으면 true) |
| validation.already_installed | boolean | `false` | 동일 `identifier` 언어팩이 이미 설치되어 있는지 여부 |
| validation.existing_version | string\|null | `null` | 이미 설치되어 있다면 그 버전 (없으면 null) |
| validation.target_version_mismatch | boolean | `false` | 대상 확장 버전이 manifest 요구 조건과 어긋나는지 여부 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "manifest 미리보기를 완료했습니다.",
"data": {
"manifest": {
"identifier": "acme-core-de",
"vendor": "acme",
"scope": "core",
"locale": "de",
"version": "1.0.0",
"name": {
"ko": "독일어 언어팩",
"en": "German Language Pack"
},
"license": "MIT"
},
"validation": {
"errors": [],
"is_valid": true,
"already_installed": false,
"existing_version": null,
"target_version_mismatch": false
}
}
}
```
**에러 응답**
@@ -584,11 +1027,33 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`LanguagePackService::refreshCache()` 반환값 — 각 캐시 계층의 무효화 성공 여부)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| registry | boolean | `true` | 언어팩 레지스트리 캐시 무효화 성공 여부 |
| translator | boolean | `true` | Laravel 번역기(loaded 번역 배열) 초기화 성공 여부 |
| template | boolean | `true` | 템플릿 언어(`template-lang`) 캐시 플러시 성공 여부 |
| version | boolean | `true` | 프론트엔드 cache busting 용 확장 캐시 버전 증가 성공 여부 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "언어팩 캐시를 갱신했습니다.",
"data": {
"registry": true,
"translator": true,
"template": true,
"version": true
}
}
```
**에러 응답**
@@ -627,11 +1092,23 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — 컨트롤러가 `success('language_packs.uninstall_success')` 만 호출하므로 `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "언어팩을 제거했습니다.",
"data": null
}
```
> 보호된 언어팩(`is_protected`)은 제거할 수 없으며, 이 경우 500 (`언어팩 제거에 실패했습니다: 보호된 언어팩은 비활성화/제거할 수 없습니다.`) 으로 응답합니다.
**에러 응답**
@@ -713,11 +1190,108 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (활성화된 언어팩 = `LanguagePackResource::toArray()`). 필드 구성은 [GET /api/admin/language-packs/{id}](#get-apiadminlanguage-packsid) 의 `manifest`/`validation_summary`/`source_meta`/`changelog_entries` 4개를 제외한 나머지와 동일합니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| identifier | string | `apidoc-sample-lang` | 언어팩 고유 식별자 |
| vendor | string | `apidoc` | 언어팩 제작자 식별자 |
| scope | string | `core` | 적용 대상 분류 (core/module/plugin/template) |
| target_identifier | string\|null | `null` | 대상 확장 식별자 (scope=core 일 때 null) |
| locale | string | `fr` | IETF BCP-47 locale 태그 |
| locale_name | string | `French` | 영문 언어명 |
| locale_native_name | string | `Français` | 원어 언어명 |
| text_direction | string | `ltr` | 텍스트 방향 (ltr/rtl) |
| version | string | `1.0.0` | 언어팩 버전 |
| latest_version | string\|null | `1.0.0` | 감지된 최신 배포 버전 |
| target_version_constraint | string\|null | `null` | 대상 확장 버전 제약 (semver) |
| target_version_mismatch | boolean | `false` | 대상 버전 불일치 경고 플래그 |
| name | string\|null | `API 문서 샘플 언어팩` | manifest `name` 의 현재 로케일 해석값 |
| license | string\|null | `MIT` | 라이선스 |
| description | string\|null | `문서 실측용 언어팩` | 언어팩 설명 (다국어) |
| status | string | `active` | 활성화 후 상태 (`active`) |
| is_protected | boolean | `false` | 보호(protected) 팩 여부 |
| source_type | string | `bundled` | 설치 소스 유형 |
| origin | string\|null | `bundled` | source_type 에서 파생된 출처 값 |
| source_url | string\|null | `null` | 설치 소스 URL 또는 경로 |
| github_url | string\|null | `null` | GitHub 저장소 URL (manifest 파생) |
| github_changelog_url | string\|null | `null` | GitHub 변경 이력(CHANGELOG) URL (manifest 파생) |
| bundled_identifier | string\|null | `null` | 대응하는 번들 확장 식별자 |
| install_blocked_reason | string\|null | `null` | 설치가 차단된 사유 (차단 없으면 null) |
| target_name | string\|null | `null` | 대상 확장의 표시 이름 |
| installed_at | string\|null | `2026-07-05 10:41:24` | 설치 일시 (사용자 타임존) |
| activated_at | string\|null | `2026-07-05 10:41:24` | 활성화 일시 (사용자 타임존) |
| created_at | string\|null | `2026-07-08 10:41:24` | 생성 일시 |
| updated_at | string\|null | `2026-07-08 10:41:24` | 최종 수정 일시 |
| has_update | boolean | `false` | `latest_version > version` 여부 |
| abilities | object | `{"can_activate":true,"can_deactivate":true,"can_uninstall":true}` | 현재 사용자가 이 언어팩에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "언어팩을 활성화했습니다.",
"data": {
"id": 1,
"identifier": "apidoc-sample-lang",
"vendor": "apidoc",
"scope": "core",
"target_identifier": null,
"locale": "fr",
"locale_name": "French",
"locale_native_name": "Français",
"text_direction": "ltr",
"version": "1.0.0",
"latest_version": "1.0.0",
"target_version_constraint": null,
"target_version_mismatch": false,
"name": "API 문서 샘플 언어팩",
"license": "MIT",
"description": "문서 실측용 언어팩",
"status": "active",
"is_protected": false,
"source_type": "bundled",
"origin": "bundled",
"source_url": null,
"github_url": null,
"github_changelog_url": null,
"bundled_identifier": null,
"install_blocked_reason": null,
"target_name": null,
"installed_at": "2026-07-05 10:41:24",
"activated_at": "2026-07-05 10:41:24",
"created_at": "2026-07-08 10:41:24",
"updated_at": "2026-07-08 10:41:24",
"has_update": false,
"abilities": {
"can_activate": true,
"can_deactivate": true,
"can_uninstall": true
}
}
}
```
**슬롯 충돌(409) 응답**
동일 슬롯(scope·target·locale)에 다른 활성 팩이 있고 `force=true` 가 아니면 409 로 응답하며, `errors` 에 현재 팩(`current`)과 대상 팩(`target`)이 위 필드 구성 그대로 담깁니다.
```json
{
"success": false,
"message": "같은 슬롯에 이미 활성 언어팩(:current)이 있습니다. (:target)으로 교체하려면 확인이 필요합니다.",
"errors": {
"current": { "id": 1, "identifier": "apidoc-sample-lang", "status": "active" },
"target": { "id": 2, "identifier": "acme-core-fr", "status": "installed" }
}
}
```
**에러 응답**
@@ -799,11 +1373,95 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (비활성화된 언어팩 = `LanguagePackResource::toArray()`). 필드 구성은 activate 응답과 동일하며 `status` 만 `inactive` 로 바뀝니다 (`activated_at` 은 마지막 활성화 시각 그대로 유지)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| identifier | string | `apidoc-sample-lang` | 언어팩 고유 식별자 |
| vendor | string | `apidoc` | 언어팩 제작자 식별자 |
| scope | string | `core` | 적용 대상 분류 (core/module/plugin/template) |
| target_identifier | string\|null | `null` | 대상 확장 식별자 (scope=core 일 때 null) |
| locale | string | `fr` | IETF BCP-47 locale 태그 |
| locale_name | string | `French` | 영문 언어명 |
| locale_native_name | string | `Français` | 원어 언어명 |
| text_direction | string | `ltr` | 텍스트 방향 (ltr/rtl) |
| version | string | `1.0.0` | 언어팩 버전 |
| latest_version | string\|null | `1.0.0` | 감지된 최신 배포 버전 |
| target_version_constraint | string\|null | `null` | 대상 확장 버전 제약 (semver) |
| target_version_mismatch | boolean | `false` | 대상 버전 불일치 경고 플래그 |
| name | string\|null | `API 문서 샘플 언어팩` | manifest `name` 의 현재 로케일 해석값 |
| license | string\|null | `MIT` | 라이선스 |
| description | string\|null | `문서 실측용 언어팩` | 언어팩 설명 (다국어) |
| status | string | `inactive` | 비활성화 후 상태 |
| is_protected | boolean | `false` | 보호(protected) 팩 여부 (보호 팩은 비활성화 불가) |
| source_type | string | `bundled` | 설치 소스 유형 |
| origin | string\|null | `bundled` | source_type 에서 파생된 출처 값 |
| source_url | string\|null | `null` | 설치 소스 URL 또는 경로 |
| github_url | string\|null | `null` | GitHub 저장소 URL (manifest 파생) |
| github_changelog_url | string\|null | `null` | GitHub 변경 이력(CHANGELOG) URL (manifest 파생) |
| bundled_identifier | string\|null | `null` | 대응하는 번들 확장 식별자 |
| install_blocked_reason | string\|null | `null` | 설치가 차단된 사유 (차단 없으면 null) |
| target_name | string\|null | `null` | 대상 확장의 표시 이름 |
| installed_at | string\|null | `2026-07-05 10:41:24` | 설치 일시 (사용자 타임존) |
| activated_at | string\|null | `2026-07-05 10:41:24` | 마지막 활성화 일시 (비활성화 시에도 초기화되지 않음) |
| created_at | string\|null | `2026-07-08 10:41:24` | 생성 일시 |
| updated_at | string\|null | `2026-07-08 10:41:24` | 최종 수정 일시 |
| has_update | boolean | `false` | `latest_version > version` 여부 |
| abilities | object | `{"can_activate":true,"can_deactivate":true,"can_uninstall":true}` | 현재 사용자가 이 언어팩에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "언어팩을 비활성화했습니다.",
"data": {
"id": 1,
"identifier": "apidoc-sample-lang",
"vendor": "apidoc",
"scope": "core",
"target_identifier": null,
"locale": "fr",
"locale_name": "French",
"locale_native_name": "Français",
"text_direction": "ltr",
"version": "1.0.0",
"latest_version": "1.0.0",
"target_version_constraint": null,
"target_version_mismatch": false,
"name": "API 문서 샘플 언어팩",
"license": "MIT",
"description": "문서 실측용 언어팩",
"status": "inactive",
"is_protected": false,
"source_type": "bundled",
"origin": "bundled",
"source_url": null,
"github_url": null,
"github_changelog_url": null,
"bundled_identifier": null,
"install_blocked_reason": null,
"target_name": null,
"installed_at": "2026-07-05 10:41:24",
"activated_at": "2026-07-05 10:41:24",
"created_at": "2026-07-08 10:41:24",
"updated_at": "2026-07-08 10:41:24",
"has_update": false,
"abilities": {
"can_activate": true,
"can_deactivate": true,
"can_uninstall": true
}
}
}
```
> 보호된 언어팩은 비활성화할 수 없으며, 이 경우 500 (`언어팩 비활성화에 실패했습니다: 보호된 언어팩은 비활성화/제거할 수 없습니다.`) 으로 응답합니다. 같은 슬롯에 다른 후보 팩이 있으면 가장 최근 inactive/installed 팩이 자동으로 활성 승격됩니다.
**에러 응답**
@@ -842,11 +1500,95 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (업데이트된 언어팩 = `LanguagePackResource::toArray()`). 필드 구성은 activate/deactivate 응답과 동일하며 `version` 이 새 버전으로 갱신되고 `has_update` 가 `false` 가 됩니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `3` | 기본 키 (내부 식별자) |
| identifier | string | `acme-core-es` | 언어팩 고유 식별자 |
| vendor | string | `acme` | 언어팩 제작자 식별자 |
| scope | string | `core` | 적용 대상 분류 (core/module/plugin/template) |
| target_identifier | string\|null | `null` | 대상 확장 식별자 (scope=core 일 때 null) |
| locale | string | `es` | IETF BCP-47 locale 태그 |
| locale_name | string | `Spanish` | 영문 언어명 |
| locale_native_name | string | `Español` | 원어 언어명 |
| text_direction | string | `ltr` | 텍스트 방향 (ltr/rtl) |
| version | string | `1.1.0` | 업데이트 후 적용된 버전 |
| latest_version | string\|null | `1.1.0` | 감지된 최신 배포 버전 |
| target_version_constraint | string\|null | `null` | 대상 확장 버전 제약 (semver) |
| target_version_mismatch | boolean | `false` | 대상 버전 불일치 경고 플래그 |
| name | string\|null | `스페인어 언어팩` | manifest `name` 의 현재 로케일 해석값 |
| license | string\|null | `MIT` | 라이선스 |
| description | string\|null | `—` | 언어팩 설명 (다국어) |
| status | string | `active` | 언어팩 상태 (업데이트 전 상태 유지) |
| is_protected | boolean | `false` | 보호(protected) 팩 여부 |
| source_type | string | `github` | 설치 소스 유형 (업데이트는 GitHub/번들 소스만 가능) |
| origin | string\|null | `github` | source_type 에서 파생된 출처 값 |
| source_url | string\|null | `https://github.com/acme/g7-core-es` | 설치 소스 URL 또는 경로 |
| github_url | string\|null | `https://github.com/acme/g7-core-es` | GitHub 저장소 URL (manifest 파생) |
| github_changelog_url | string\|null | `null` | GitHub 변경 이력(CHANGELOG) URL (manifest 파생) |
| bundled_identifier | string\|null | `null` | 대응하는 번들 확장 식별자 |
| install_blocked_reason | string\|null | `null` | 설치가 차단된 사유 (차단 없으면 null) |
| target_name | string\|null | `null` | 대상 확장의 표시 이름 |
| installed_at | string\|null | `2026-07-05 10:41:24` | 설치 일시 (사용자 타임존) |
| activated_at | string\|null | `2026-07-05 10:41:24` | 활성화 일시 (사용자 타임존) |
| created_at | string\|null | `2026-07-05 10:41:24` | 생성 일시 |
| updated_at | string\|null | `2026-07-09 09:12:00` | 최종 수정 일시 (업데이트 시각으로 갱신) |
| has_update | boolean | `false` | `latest_version > version` 여부 (업데이트 직후 false) |
| abilities | object | `{"can_activate":true,"can_deactivate":true,"can_uninstall":true}` | 현재 사용자가 이 언어팩에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "언어팩을 업데이트했습니다.",
"data": {
"id": 3,
"identifier": "acme-core-es",
"vendor": "acme",
"scope": "core",
"target_identifier": null,
"locale": "es",
"locale_name": "Spanish",
"locale_native_name": "Español",
"text_direction": "ltr",
"version": "1.1.0",
"latest_version": "1.1.0",
"target_version_constraint": null,
"target_version_mismatch": false,
"name": "스페인어 언어팩",
"license": "MIT",
"description": null,
"status": "active",
"is_protected": false,
"source_type": "github",
"origin": "github",
"source_url": "https://github.com/acme/g7-core-es",
"github_url": "https://github.com/acme/g7-core-es",
"github_changelog_url": null,
"bundled_identifier": null,
"install_blocked_reason": null,
"target_name": null,
"installed_at": "2026-07-05 10:41:24",
"activated_at": "2026-07-05 10:41:24",
"created_at": "2026-07-05 10:41:24",
"updated_at": "2026-07-09 09:12:00",
"has_update": false,
"abilities": {
"can_activate": true,
"can_deactivate": true,
"can_uninstall": true
}
}
}
```
> 업데이트 소스 정보가 없거나(`업데이트 소스 정보가 없습니다 (GitHub 소스 언어팩만 업데이트 가능).`) 이미 최신 버전이면(`이미 최신 버전입니다.`) 500 (`언어팩 업데이트에 실패했습니다: :error`) 으로 응답합니다.
**에러 응답**
+129 -9
View File
@@ -67,7 +67,7 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
| --- | --- | --- | --- | --- | --- |
| token | path | string | 예 | — | 인증/검증 토큰 |
| token | path | string | 예 | — | 미리보기 토큰(UUID). `LayoutPreviewService::createPreview` 가 발급하며 기본 30분 후 만료 |
**요청 예시**
@@ -80,17 +80,76 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `success`/`message`/`data` 봉투를 사용하지 않습니다. `LayoutPreviewController@serve` 가 `response()->json($layout)` 로 **병합된 레이아웃 JSON 객체를 최상위(root)에 그대로** 반환합니다 (상속 병합 + 모듈/플러그인 확장 적용 완료 상태). 아래는 레이아웃 JSON의 최상위 키이며, 실제 포함 여부는 저장된 레이아웃 content 에 따라 달라집니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| version | string | `"1.0"` | 레이아웃 스키마 버전 (저장 시 필수) |
| layout_name | string | `"product_list"` | 레이아웃 이름 (식별자) |
| extends | string\|null | `"_user_base"` | 상속할 베이스 레이아웃 이름 (미리보기 서빙 시 이미 병합됨) |
| slots | object | `{"content": [ ... ]}` | 베이스 레이아웃 슬롯에 삽입할 컴포넌트 배열 (extends 레이아웃) |
| components | array | `[{"type":"basic","name":"Div", ...}]` | 렌더링할 컴포넌트 트리 (병합·확장 적용 결과) |
| endpoint | string\|null | `"/api/products"` | 화면이 주로 fetch 하는 데이터 API 경로 (레거시·선택) |
| data_sources | array | `[{"id":"products","endpoint":"/api/products","method":"GET"}]` | 화면이 로드하는 API 데이터 소스 정의 |
| meta | object | `{"title":"상품 목록","auth_required":false}` | 메타 정보 (title, description, keywords, auth_required, is_base, guest_only, is_error_layout, error_code, seo) |
| permissions | array\|object | `["core.templates.layouts.edit"]` | 레이아웃 접근에 필요한 권한 (flat 배열=AND, 구조화 객체=OR/AND 중첩) |
| modals | object | `{}` | 모달 컴포넌트 정의 |
| state / init_state / initLocal / initGlobal / initIsolated / global_state | object | `{}` | 레이아웃 레벨 초기 상태 정의 |
| init_actions | array | `[{"handler":"apiCall", ...}]` | 레이아웃 로드 시 실행할 초기화 액션 |
| actions / named_actions | array\|object | `{}` | 재사용 가능한 액션 정의 |
| computed | object | `{}` | 계산된 값 정의 |
| defines | object | `{}` | 재사용 가능한 컴포넌트 조각 정의 |
| errorHandling | object | `{"404":{"handler":"navigate"}}` | 상태 코드별 에러 처리 정책 |
| globalHeaders | array | `[{"pattern":"*","headers":{"X-Foo":"bar"}}]` | API 호출 시 자동 적용되는 HTTP 헤더 규칙 |
| transition_overlay | object | `{"enabled":true,"style":"blur"}` | 페이지 전환 오버레이 설정 |
| routes | array | `[]` | 라우트 정의 |
| pageConfig / schema | object | `{}` | 플러그인 설정 레이아웃 전용 (설정 UI 안내/스키마) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"version": "1.0",
"layout_name": "product_list",
"meta": {
"title": "상품 목록",
"auth_required": false
},
"data_sources": [
{
"id": "products",
"endpoint": "/api/products",
"method": "GET"
}
],
"components": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "p-4"
},
"children": []
}
]
}
```
404 응답은 봉투 형식입니다 (`BaseApiController::notFound` → `templates.layout_not_found`).
```json
{
"success": false,
"message": "레이아웃을 찾을 수 없습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | 토큰에 해당하는 미리보기가 없거나 만료된 경우 (`templates.layout_not_found`) |
<!-- @generated:end -->
@@ -148,8 +207,8 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
| --- | --- | --- | --- | --- | --- |
| templateIdentifier | path | string | 예 | — | 대상 template의 식별자 |
| layoutName | path | string | 예 | — | 대상 layout의 이름 (식별자) |
| templateIdentifier | path | string | 예 | — | 레이아웃을 서빙할 활성 템플릿의 식별자 (예: `sirsoft-basic`) — 미존재/비활성 시 404 |
| layoutName | path | string | 예 | — | 서빙할 레이아웃 이름 (예: `product_list`) |
**요청 예시**
@@ -162,17 +221,78 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 는 병합된 레이아웃 JSON 객체입니다 (상속 병합 + 모듈/플러그인 확장 적용 + 사용자별 컴포넌트 권한 필터링 완료). 실제 포함 키는 저장된 레이아웃 content 에 따라 달라집니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| version | string | `"1.0"` | 레이아웃 스키마 버전 |
| layout_name | string | `"product_list"` | 레이아웃 이름 (식별자) |
| extends | string\|null | `"_user_base"` | 상속한 베이스 레이아웃 이름 (서빙 시 이미 병합 반영됨) |
| slots | object | `{"content": [ ... ]}` | 베이스 슬롯에 삽입되는 컴포넌트 배열 (extends 레이아웃) |
| components | array | `[{"type":"basic","name":"Div", ...}]` | 렌더링할 컴포넌트 트리 (병합·확장·권한 필터 적용 결과) |
| endpoint | string\|null | `"/api/products"` | 화면이 주로 fetch 하는 데이터 API 경로 (레거시·선택) |
| data_sources | array | `[{"id":"products","endpoint":"/api/products","method":"GET"}]` | 화면이 로드하는 API 데이터 소스 정의 |
| meta | object | `{"title":"상품 목록","auth_required":false}` | 메타 정보 (title, description, keywords, auth_required, is_base, guest_only, is_error_layout, error_code, seo) |
| permissions | array\|object | `["core.templates.layouts.edit"]` | 레이아웃 접근에 필요한 권한 (flat 배열=AND, 구조화 객체=OR/AND 중첩) |
| modals | object | `{}` | 모달 컴포넌트 정의 |
| state / init_state / initLocal / initGlobal / initIsolated / global_state | object | `{}` | 레이아웃 레벨 초기 상태 정의 |
| init_actions | array | `[{"handler":"apiCall", ...}]` | 레이아웃 로드 시 실행할 초기화 액션 |
| actions / named_actions | array\|object | `{}` | 재사용 가능한 액션 정의 |
| computed | object | `{}` | 계산된 값 정의 |
| defines | object | `{}` | 재사용 가능한 컴포넌트 조각 정의 |
| errorHandling | object | `{"404":{"handler":"navigate"}}` | 상태 코드별 에러 처리 정책 |
| globalHeaders | array | `[{"pattern":"*","headers":{"X-Foo":"bar"}}]` | API 호출 시 자동 적용되는 HTTP 헤더 규칙 |
| transition_overlay | object | `{"enabled":true,"style":"blur"}` | 페이지 전환 오버레이 설정 |
| routes | array | `[]` | 라우트 정의 |
| pageConfig / schema | object | `{}` | 플러그인 설정 레이아웃 전용 (설정 UI 안내/스키마) |
| lock_version | integer | `3` | 낙관적 잠금 버전. `with_source_meta=1` 일 때만 부착 (편집기 저장 시 `expected_lock_version` 으로 되돌려 보냄) |
| __editor | object | `{"original": { ... }}` | 자식 레이아웃의 저장 원본 content. `with_source_meta=1` 일 때만 부착 (편집기 전용) |
| __source (각 노드 내부) | object | `{"kind":"base","layout":"_user_base"}` | 각 컴포넌트/데이터소스 노드의 출처 메타 (`base` / `extension` / `partial` / `route`). `with_source_meta=1` 일 때만 부착 |
응답 헤더: `ETag`(본문 md5), `Cache-Control: public, max-age=3600`, `Vary: Accept-Encoding, Accept-Language`. 클라이언트 `If-None-Match` 가 일치하면 본문 없이 `304 Not Modified` 를 반환합니다.
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "레이아웃이 제공되었습니다.",
"data": {
"version": "1.0",
"layout_name": "product_list",
"meta": {
"title": "상품 목록",
"auth_required": false
},
"data_sources": [
{
"id": "products",
"endpoint": "/api/products",
"method": "GET"
}
],
"components": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "p-4"
},
"children": []
}
]
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 304 | Not Modified | 요청의 `If-None-Match` 가 현재 ETag 와 일치 (본문 없음) |
| 401 | Unauthorized | 비회원이 `permissions` 요구 레이아웃에 접근 (`auth.layout_guest_permission_denied`), 또는 비회원이 `with_source_meta=1` 요청 |
| 403 | Forbidden | 회원이지만 레이아웃 `permissions` 미충족 (`auth.layout_permission_denied`), 또는 `with_source_meta=1` 요청 시 `core.templates.layouts.edit` 권한 없음 |
| 404 | Not Found | 템플릿이 없거나 비활성 상태, 또는 레이아웃/부모 레이아웃 미존재 (`templates.layout_not_found`) |
<!-- @generated:end -->
+1 -1
View File
@@ -69,7 +69,7 @@ HTTP/1.1 200
**에러 응답**
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
_이 엔드포인트는 도메인 에러를 반환하지 않습니다. 공개 조회이므로 인증 실패(401)가 없고, 요청 파라미터가 없어 검증 실패(422)도 발생하지 않으며, 컨트롤러(`LocaleController@active`)와 `LanguagePackService::getActiveLocales()` 경로에서 던지는 예외도 없습니다._
<!-- @generated:end -->
+82 -4
View File
@@ -38,11 +38,17 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-500 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: http-500 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "회원 탈퇴가 완료되었습니다.",
"data": null
}
```
**에러 응답**
@@ -309,11 +315,83 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`UserResource::toArray()` 산물 — GET /api/me 의 `toProfileArray()` 와 필드 구성이 다르다)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| uuid | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) |
| name | string | `홍길동` | 사용자 이름 |
| nickname | string\|null | `gildong` | 닉네임 |
| email | string | `user@example.com` | 이메일 주소 |
| avatar | string\|null | `null` | 아바타 이미지 URL (`User::getAvatarUrl()` 산물, 미등록 시 null) |
| language | string | `ko` | 사용자 언어 설정 (ko: 한국어, en: 영어) |
| language_label | string\|null | `한국어` | 언어 코드의 다국어 라벨 (`user.language.{code}` 번역문) |
| country | string\|null | `KR` | 국가 코드 (ISO 3166-1 alpha-2) |
| status | string | `active` | 계정 상태 (active, inactive, blocked, withdrawn, pending_verification) |
| status_label | string\|null | `활성` | 상태의 사람이 읽는 라벨 (UserStatus::label() 산물) |
| status_variant | string\|null | `success` | 상태 표시 색상/스타일 변형 키 (UserStatus::variant() 산물 — UI 배지용) |
| is_admin | boolean | `false` | 관리자 역할 보유 여부 (`User::isAdmin()` — 역할 관계 기반 파생) |
| homepage | string\|null | `https://example.com` | 홈페이지 URL |
| mobile | string\|null | `010-1234-5678` | 휴대폰 번호 |
| phone | string\|null | `02-123-4567` | 전화번호 |
| zipcode | string\|null | `06234` | 우편번호 |
| address | string\|null | `서울특별시 강남구 테헤란로 1` | 기본 주소 |
| address_detail | string\|null | `101동 202호` | 상세 주소 |
| signature | string\|null | `null` | 서명 |
| bio | string\|null | `null` | 자기소개 |
| last_login_at | string\|null | `2026-07-07 10:41:24` | 마지막 로그인 일시 (사용자 시간대 기준 문자열) |
| email_verified_at | string\|null | `null` | 이메일 인증 일시 (미인증 시 null) |
| timezone | string\|null | `Asia/Seoul` | 사용자 시간대 (예: Asia/Seoul, UTC) |
| created_at | string | `2026-07-08 10:41:24` | 생성 일시 (사용자 시간대 기준 문자열) |
| updated_at | string | `2026-07-08 11:02:10` | 수정 일시 (사용자 시간대 기준 문자열) |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_read":false,"can_create":false,"can_update":false,"can_delete":false,"can_assign_roles":false}` | 현재 사용자의 이 리소스에 대한 권한 맵 (core.users.read/create/update/delete, core.permissions.update 기준. 슈퍼관리자 계정은 `can_delete` 가 항상 false) |
관계형 필드(`modules`, `plugins`, `menus`, `roles`, `permissions`, `consents`, `terms_consent`, `privacy_consent`)와 카운트 필드(`modules_count`, `plugins_count`, `menus_count`)는 해당 관계가 로드된 경우에만 응답에 포함된다 (프로필 수정 응답에서는 로드하지 않으므로 나타나지 않는다).
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "사용자 정보가 성공적으로 업데이트되었습니다.",
"data": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "예시 이름",
"nickname": "예시 이름",
"email": "user@example.com",
"avatar": null,
"language": "ko",
"language_label": "한국어",
"country": "KR",
"status": "active",
"status_label": "활성",
"status_variant": "success",
"is_admin": false,
"homepage": "https://example.com",
"mobile": "010-1234-5678",
"phone": "010-1234-5678",
"zipcode": "06234",
"address": "서울특별시 강남구 테헤란로 1",
"address_detail": "서울특별시 강남구 테헤란로 1",
"signature": "예시값",
"bio": "예시 내용입니다.",
"last_login_at": "2026-07-07 10:41:24",
"email_verified_at": null,
"timezone": "Asia/Seoul",
"created_at": "2026-07-08 10:41:24",
"updated_at": "2026-07-08 11:02:10",
"is_owner": true,
"abilities": {
"can_read": false,
"can_create": false,
"can_update": false,
"can_delete": false,
"can_assign_roles": false
}
}
}
```
**에러 응답**
+171 -8
View File
@@ -481,11 +481,94 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_목록 응답: `data.data[]` 배열 항목의 필드 + 컬렉션 레벨 `data.abilities`._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `15` | 기본 키 (내부 식별자) |
| name | object | `{"ko":"게시판 관리","en":"Board Management"}` | 메뉴 이름 (다국어 JSON) |
| slug | string | `sirsoft-board` | 메뉴 슬러그 |
| url | string \| null | `null` | 메뉴 URL (그룹 메뉴는 null) |
| icon | string \| null | `fas fa-clipboard-list` | 메뉴 아이콘 (Font Awesome 클래스) |
| order | integer | `30` | 메뉴 순서 (작을수록 우선 — `order` 오름차순 정렬) |
| is_active | boolean | `true` | 활성 여부 |
| parent_id | integer \| null | `null` | 상위 메뉴 ID (최상위는 null) |
| extension_type | string | `module` | 확장 소유 타입 (`module` / `plugin`) |
| extension_identifier | string | `sirsoft-board` | 확장 식별자 (요청 path 의 `identifier` 와 동일) |
| parent | object \| null | `null` | 상위 항목 객체 (id/name/url/icon — parent 관계 파생) |
| children | array | `[{"id":16,"name":{"ko":"환경설정"},"slug":"sirsoft-board-settings", ...}]` | 하위 메뉴 배열 (order 오름차순 — children 관계 파생) |
| creator | object \| null | `null` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) |
| created_at | string | `2026-07-08 10:44:35` | 생성 일시 |
| updated_at | string | `2026-07-08 10:44:35` | 최종 수정 일시 |
| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
> `roles` 관계는 이 엔드포인트에서 eager-load 되지 않으므로(`MenuRepository::getMenusByExtension` 은 `creator`/`parent`/`children` 만 로드) 항목에 `roles` 키가 포함되지 않습니다.
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "메뉴를 성공적으로 가져왔습니다.",
"data": {
"data": [
{
"id": 15,
"name": {
"ko": "게시판 관리",
"en": "Board Management"
},
"slug": "sirsoft-board",
"url": null,
"icon": "fas fa-clipboard-list",
"order": 30,
"is_active": true,
"parent_id": null,
"extension_type": "module",
"extension_identifier": "sirsoft-board",
"parent": null,
"children": [
{
"id": 16,
"name": {
"ko": "환경설정",
"en": "Settings"
},
"slug": "sirsoft-board-settings",
"url": "/admin/boards/settings",
"icon": "fas fa-cog",
"order": 1,
"is_active": true,
"parent_id": 15,
"extension_type": "module",
"extension_identifier": "sirsoft-board",
"roles": []
}
],
"creator": null,
"created_at": "2026-07-08 10:44:35",
"updated_at": "2026-07-08 10:44:35",
"is_owner": false,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
],
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -638,11 +721,21 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — 컨트롤러가 `success('menu.order_update_success')` 를 데이터 없이 호출)._
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "메뉴 순서가 성공적으로 업데이트되었습니다.",
"data": null
}
```
**에러 응답**
@@ -650,7 +743,7 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.menus.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `moved_items[].new_parent_id` 가 자기 자신·자손을 가리키는 순환 참조인 경우 포함 |
<!-- @generated:end -->
@@ -682,7 +775,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — 컨트롤러가 `success('menu.delete_success')` 를 데이터 없이 호출)._
**응답 예시**
@@ -928,11 +1021,81 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (갱신된 메뉴를 `MenuResource` 로 반환 — `creator`/`parent`/`children`/`roles` eager-load)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| name | object | `{"ko":"API 문서 샘플 메뉴","en":"API Doc Sample Menu"}` | 메뉴 이름 (다국어 JSON) |
| slug | string | `apidoc-sample-menu` | 메뉴 슬러그 |
| url | string \| null | `/admin/apidoc-sample` | 메뉴 URL |
| icon | string \| null | `fas fa-book` | 메뉴 아이콘 |
| order | integer | `35` | 메뉴 순서 |
| is_active | boolean | `true` | 활성 여부 |
| parent_id | integer \| null | `null` | 상위 메뉴 ID |
| extension_type | string \| null | `core` | 확장 소유 타입: core / module / plugin / null(사용자 정의) |
| extension_identifier | string \| null | `core` | 확장 식별자 |
| parent | object \| null | `null` | 상위 항목 객체 (id/name/url/icon — parent 관계 파생) |
| children | array | `[]` | 하위 항목 배열 (order 오름차순 — children 관계 파생) |
| creator | object \| null | `{"uuid":"a234c2b1-…","name":"API 문서 샘플 사용자","email":"apidoc-sample-user@example.com"}` | 생성자 정보 객체 (creator 관계 파생) |
| roles | array | `[{"id":1,"name":{"ko":"관리자","en":"Administrator"},"permission_type":"read"}]` | 이 메뉴 노출이 허용된 역할 목록 (요청 `roles` 로 교체된 결과) |
| created_at | string | `2026-07-08 10:41:24` | 생성 일시 |
| updated_at | string | `2026-07-08 12:20:11` | 최종 수정 일시 |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_create":true,"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 Menu"
},
"slug": "apidoc-sample-menu",
"url": "/admin/apidoc-sample",
"icon": "fas fa-book",
"order": 35,
"is_active": true,
"parent_id": null,
"extension_type": null,
"extension_identifier": null,
"parent": null,
"children": [],
"creator": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com"
},
"roles": [
{
"id": 1,
"name": {
"ko": "관리자",
"en": "Administrator"
},
"permission_type": "read"
}
],
"created_at": "2026-07-08 10:41:24",
"updated_at": "2026-07-08 12:20:11",
"is_owner": true,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
File diff suppressed because it is too large Load Diff
+86 -2
View File
@@ -369,11 +369,95 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드. 수정된 알림 정의를 `templates` 관계와 함께 반환합니다 (`NotificationDefinitionResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| type | string | `apidoc-sample.event` | 알림 타입 (welcome, order_confirmed 등) |
| hook_prefix | string | `core` | 훅 접두사 (core.auth, sirsoft-ecommerce 등) |
| extension_type | string | `core` | 이 정의를 소유한 확장의 타입 (core/module/plugin) |
| extension_identifier | string | `` | 이 정의를 소유한 확장의 식별자 (코어는 빈 문자열) |
| name | object | `{"ko":"API 문서 샘플 알림","en":"API Doc Sample Notification"}` | 다국어 이름 (로케일별 값 객체) |
| description | object | `{"ko":"문서 실측용 알림 정의","en":"Sample notification"}` | 다국어 설명 (로케일별 값 객체) |
| variables | array | `[]` | 사용 가능 변수 메타데이터 ([{key, description}]) |
| channels | array | `["database","mail"]` | 수정 후의 활성 채널 (["mail", "database"]) |
| hooks | array | `[]` | 수정 후의 트리거 훅 목록 (["core.auth.after_register"]) |
| is_active | boolean | `true` | active 여부 |
| is_default | boolean | `false` | default 여부 (사용자 수정 시 false) |
| templates | array | `[{"id":1,"definition_id":1,"channel":"mail", ...}]` | 채널별 알림 템플릿 목록 (`templates` 관계를 로드해 반환 — NotificationTemplateResource 배열) |
| created_at | string | `2026-07-08 10:41:24` | 생성 일시 |
| updated_at | string | `2026-07-08 12:14:43` | 최종 수정 일시 |
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update/can_delete — 모두 `core.settings.update` 기준) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "알림 정의가 수정되었습니다.",
"data": {
"id": 1,
"type": "apidoc-sample.event",
"hook_prefix": "core",
"extension_type": "core",
"extension_identifier": "",
"name": {
"ko": "API 문서 샘플 알림",
"en": "API Doc Sample Notification"
},
"description": {
"ko": "문서 실측용 알림 정의",
"en": "Sample notification"
},
"variables": [],
"channels": [
"database",
"mail"
],
"hooks": [],
"is_active": true,
"is_default": false,
"templates": [
{
"id": 1,
"definition_id": 1,
"channel": "mail",
"subject": "API 문서 샘플 템플릿 제목",
"body": "안녕하세요 {{name}} 님, 문서 실측용 본문입니다.",
"click_url": "/admin/apidoc-sample",
"recipients": [
{
"type": "role",
"value": "admin",
"display_name": "관리자"
}
],
"is_active": true,
"is_default": false,
"user_overrides": null,
"updated_by": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"created_at": "2026-07-08 10:41:24",
"updated_at": "2026-07-08 10:41:24",
"abilities": {
"can_update": true,
"can_delete": true
}
}
],
"created_at": "2026-07-08 10:41:24",
"updated_at": "2026-07-08 12:14:43",
"abilities": {
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
+19 -5
View File
@@ -208,11 +208,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| deleted_count | integer | `3` | 실제로 삭제된 알림 발송 이력 건수 (요청한 `ids` 중 존재하는 행만 카운트) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "선택한 알림 발송 이력이 삭제되었습니다.",
"data": {
"deleted_count": 3
}
}
```
**에러 응답**
@@ -250,9 +266,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — 컨트롤러가 `success(__('notification_log.delete_success'))` 만 호출하므로 `data` 는 `null`)._
**응답 예시**
+74 -4
View File
@@ -55,11 +55,25 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| subject | string | `[사이트명] 회원가입을 환영합니다` | 요청 `locale`(미지정 시 현재 앱 로케일) 의 제목에서 `{변수}` 를 정의(`definition_id`)의 변수 설명(`[설명]`)으로 치환한 결과 |
| body | string | `<h1>[회원 이름]님, 환영합니다!</h1>...` | 동일 로케일 본문에서 `{변수}` 를 정의의 변수 설명으로 치환한 결과. 해당 로케일 값이 없으면 빈 문자열 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "미리보기를 생성했습니다.",
"data": {
"subject": "[사이트명] 회원가입을 환영합니다",
"body": "<h1>[회원 이름]님, 환영합니다!</h1><p>[사이트명]에 가입해 주셔서 감사합니다.</p>"
}
}
```
**에러 응답**
@@ -119,11 +133,67 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`NotificationTemplateResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| definition_id | integer | `1` | 소속 알림 정의 식별자 |
| channel | string | `mail` | 발송 채널 (`mail`, `database`, `fcm`) |
| subject | object | `{"ko":"[{app_name}] 회원가입을 환영합니다","en":"..."}` | 다국어 제목 (로케일 키 → 문자열, 로케일당 최대 500자) |
| body | object | `{"ko":"<h1>{name}님, 환영합니다!</h1>...","en":"..."}` | 다국어 본문 (로케일 키 → 문자열, 로케일당 최대 65535자) |
| click_url | string \| null | `null` | 알림 클릭 시 이동할 대상 URL (미설정 시 `null`) |
| recipients | array | `[{"type":"role","value":"admin","display_name":"관리자"}]` | 수신자 규칙 목록. `type` 은 `trigger_user`/`related_user`/`role`/`specific_users`. `role` 이면 `display_name`, `specific_users` 면 `display_names` 가 표시용으로 부가됨 |
| is_active | boolean | `true` | 활성 여부 (false 면 해당 채널 발송 중단) |
| is_default | boolean | `false` | 기본값 상태 여부 (수정 시 항상 `false` 로 전환됨) |
| user_overrides | array \| null | `["subject","body"]` | 사용자가 직접 수정한 필드명 목록 (`HasUserOverrides`) |
| updated_by | string \| null | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 최종 수정한 사용자 UUID (없으면 `null`) |
| created_at | string | `2026-07-08 10:41:24` | 생성 일시 |
| updated_at | string | `2026-07-08 12:14:43` | 최종 수정 일시 |
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (모두 `core.settings.update` 권한 기반) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "알림 템플릿이 수정되었습니다.",
"data": {
"id": 1,
"definition_id": 1,
"channel": "mail",
"subject": {
"ko": "[{app_name}] 회원가입을 환영합니다",
"en": "[{app_name}] Welcome to Our Service"
},
"body": {
"ko": "<h1>{name}님, 환영합니다!</h1>",
"en": "<h1>Welcome, {name}!</h1>"
},
"click_url": "/admin/notifications",
"recipients": [
{
"type": "role",
"value": "admin",
"display_name": "관리자"
}
],
"is_active": true,
"is_default": false,
"user_overrides": [
"subject",
"body"
],
"updated_by": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"created_at": "2026-07-08 10:41:24",
"updated_at": "2026-07-08 12:14:43",
"abilities": {
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
+164 -32
View File
@@ -49,16 +49,16 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| number | integer | `23` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) |
| id | string | `518b9c94-5853-4b68-8193-83af1851bbc6` | 기본 키 (내부 식별자) |
| type | string | `inquiry_received` | <!-- TODO: 설명 --> |
| type_label | string | `상품 문의 접수` | `type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) |
| subject | string | `새로운 상품 문의가 접수되었습니다` | <!-- TODO: 설명 --> |
| body | string | `옥혜진님이 "면 손수건 3매입 #1" 상품에 문의를 남겼습니다.` | <!-- TODO: 설명 --> |
| url | string | `https://g7_2.dev/admin/ecommerce/prod…` | <!-- TODO: 설명 --> |
| data | object | `{"type":"inquiry_received","subject":"새로운 상품 문의가 접수되었습니다"…` | <!-- TODO: 설명 --> |
| read_at | string | `2026-07-31 22:06:53` | read 일시 |
| created_at | string | `2026-07-31 21:41:58` | 생성 일시 |
| number | integer | `92` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) |
| id | string | `02ee1121-ec63-4812-b77a-a0ffb4040acb` | 기본 키 (내부 식별자) |
| type | string | `new_order_admin` | 알림 유형 식별자 (알림 정의의 type — 발송 트리거를 구분) |
| type_label | string | `신규 주문 관리자 알림` | `type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) |
| subject | string | `새로운 주문이 접수되었습니다` | 알림 제목 (알림 템플릿의 subject 를 변수 치환해 렌더한 문자열) |
| body | string | `소경은님이 주문번호 20260713-0038038875 (결제금액:…` | 알림 본문 (알림 템플릿의 body 를 변수 치환해 렌더한 문자열) |
| url | string | `https://g7.dev/admin/ecommerce/orders…` | 알림 클릭 시 이동할 URL (템플릿의 click_url 우선, 없으면 메일 CTA action_url) |
| data | object | `{"type":"new_order_admin","subject":"새로운 주문이 접수되었습니다","bo…` | 알림 원본 페이로드 객체 (type/subject/body/click_url 과 발송 시 전달된 변수 data 를 포함) |
| read_at | null | `null` | read 일시 |
| created_at | string | `2026-07-13 09:38:04` | 생성 일시 |
**응답 예시**
@@ -302,11 +302,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| marked_count | integer | `2` | 실제로 읽음 처리된 알림 건수 (요청한 `ids` 중 본인 소유이면서 미읽음 상태였던 항목만 집계) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "알림을 읽음 처리했습니다.",
"data": {
"marked_count": 2
}
}
```
**에러 응답**
@@ -404,11 +420,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
}
```
**에러 응답**
@@ -449,11 +475,51 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (읽음 처리 후 갱신된 알림 1건 — `UserNotificationResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | string | `02ee1121-ec63-4812-b77a-a0ffb4040acb` | 알림 기본 키 (UUID) |
| type | string | `new_order_admin` | 알림 유형 식별자 (알림 정의의 type — 발송 트리거를 구분) |
| type_label | string | `신규 주문 관리자 알림` | `type` 값의 사람이 읽는 라벨 (알림 정의의 현지화 이름, 정의 미존재 시 빈 문자열) |
| subject | string | `새로운 주문이 접수되었습니다` | 알림 제목 (알림 템플릿의 subject 를 변수 치환해 렌더한 문자열) |
| body | string | `소경은님이 주문번호 20260713-0038038875 (결제금액:…` | 알림 본문 (알림 템플릿의 body 를 변수 치환해 렌더한 문자열) |
| url | string \| null | `https://g7.dev/admin/ecommerce/orders/1` | 알림 클릭 시 이동할 URL (템플릿의 `click_url` 우선, 없으면 메일 CTA `action_url`, 둘 다 없으면 `null`) |
| data | object | `{"type":"new_order_admin","subject":"...","body":"...","click_url":"..."}` | 알림 원본 페이로드 객체 (type/subject/body/click_url 과 발송 시 전달된 변수 data 를 포함) |
| read_at | string \| null | `2026-07-13 09:40:11` | 읽음 처리 일시 (사용자 타임존 기준 `Y-m-d H:i:s`) — 이 엔드포인트 성공 시 항상 값이 채워짐 |
| created_at | string \| null | `2026-07-13 09:38:04` | 알림 생성 일시 (사용자 타임존 기준 `Y-m-d H:i:s`) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "알림을 읽음 처리했습니다.",
"data": {
"id": "02ee1121-ec63-4812-b77a-a0ffb4040acb",
"type": "new_order_admin",
"type_label": "신규 주문 관리자 알림",
"subject": "새로운 주문이 접수되었습니다",
"body": "소경은님이 주문번호 20260713-0038038875 주문을 접수했습니다.",
"url": "https://g7.dev/admin/ecommerce/orders/1",
"data": {
"type": "new_order_admin",
"subject": "새로운 주문이 접수되었습니다",
"body": "소경은님이 주문번호 20260713-0038038875 주문을 접수했습니다.",
"click_url": "https://g7.dev/admin/ecommerce/orders/1",
"data": {
"action_url": "https://g7.dev/admin/ecommerce/orders/1"
}
},
"read_at": "2026-07-13 09:40:11",
"created_at": "2026-07-13 09:38:04"
}
}
```
**에러 응답**
@@ -503,16 +569,16 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| number | integer | `23` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) |
| id | string | `518b9c94-5853-4b68-8193-83af1851bbc6` | 기본 키 (내부 식별자) |
| type | string | `inquiry_received` | <!-- TODO: 설명 --> |
| type_label | string | `상품 문의 접수` | `type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) |
| subject | string | `새로운 상품 문의가 접수되었습니다` | <!-- TODO: 설명 --> |
| body | string | `옥혜진님이 "면 손수건 3매입 #1" 상품에 문의를 남겼습니다.` | <!-- TODO: 설명 --> |
| url | string | `https://g7_2.dev/admin/ecommerce/prod…` | <!-- TODO: 설명 --> |
| data | object | `{"type":"inquiry_received","subject":"새로운 상품 문의가 접수되었습니다"…` | <!-- TODO: 설명 --> |
| read_at | string | `2026-07-31 22:06:53` | read 일시 |
| created_at | string | `2026-07-31 21:41:58` | 생성 일시 |
| number | integer | `92` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) |
| id | string | `02ee1121-ec63-4812-b77a-a0ffb4040acb` | 기본 키 (내부 식별자) |
| type | string | `new_order_admin` | 알림 유형 식별자 (알림 정의의 type — 발송 트리거를 구분) |
| type_label | string | `신규 주문 관리자 알림` | `type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) |
| subject | string | `새로운 주문이 접수되었습니다` | 알림 제목 (알림 템플릿의 subject 를 변수 치환해 렌더한 문자열) |
| body | string | `소경은님이 주문번호 20260713-0038038875 (결제금액:…` | 알림 본문 (알림 템플릿의 body 를 변수 치환해 렌더한 문자열) |
| url | string | `https://g7.dev/admin/ecommerce/orders…` | 알림 클릭 시 이동할 URL (템플릿의 click_url 우선, 없으면 메일 CTA action_url) |
| data | object | `{"type":"new_order_admin","subject":"새로운 주문이 접수되었습니다","bo…` | 알림 원본 페이로드 객체 (type/subject/body/click_url 과 발송 시 전달된 변수 data 를 포함) |
| read_at | null | `null` | read 일시 |
| created_at | string | `2026-07-13 09:38:04` | 생성 일시 |
**응답 예시**
@@ -756,11 +822,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| marked_count | integer | `2` | 실제로 읽음 처리된 알림 건수 (요청한 `ids` 중 본인 소유이면서 미읽음 상태였던 항목만 집계) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "알림을 읽음 처리했습니다.",
"data": {
"marked_count": 2
}
}
```
**에러 응답**
@@ -858,11 +940,21 @@ 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
}
```
**에러 응답**
@@ -903,11 +995,51 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (읽음 처리 후 갱신된 알림 1건 — `UserNotificationResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | string | `9f2c1a44-1b6e-4a55-8b0e-4c8a3f1d90ab` | 알림 기본 키 (UUID) |
| type | string | `order_shipped` | 알림 유형 식별자 (알림 정의의 type — 발송 트리거를 구분) |
| type_label | string | `배송 시작 알림` | `type` 값의 사람이 읽는 라벨 (알림 정의의 현지화 이름, 정의 미존재 시 빈 문자열) |
| subject | string | `주문하신 상품이 발송되었습니다` | 알림 제목 (알림 템플릿의 subject 를 변수 치환해 렌더한 문자열) |
| body | string | `주문번호 20260713-0038038875 상품이 발송되었습니다.` | 알림 본문 (알림 템플릿의 body 를 변수 치환해 렌더한 문자열) |
| url | string \| null | `https://g7.dev/mypage/orders/1` | 알림 클릭 시 이동할 URL (템플릿의 `click_url` 우선, 없으면 메일 CTA `action_url`, 둘 다 없으면 `null`) |
| data | object | `{"type":"order_shipped","subject":"...","body":"...","click_url":"..."}` | 알림 원본 페이로드 객체 (type/subject/body/click_url 과 발송 시 전달된 변수 data 를 포함) |
| read_at | string \| null | `2026-07-13 10:02:33` | 읽음 처리 일시 (사용자 타임존 기준 `Y-m-d H:i:s`) — 이 엔드포인트 성공 시 항상 값이 채워짐 |
| created_at | string \| null | `2026-07-13 09:58:12` | 알림 생성 일시 (사용자 타임존 기준 `Y-m-d H:i:s`) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "알림을 읽음 처리했습니다.",
"data": {
"id": "9f2c1a44-1b6e-4a55-8b0e-4c8a3f1d90ab",
"type": "order_shipped",
"type_label": "배송 시작 알림",
"subject": "주문하신 상품이 발송되었습니다",
"body": "주문번호 20260713-0038038875 상품이 발송되었습니다.",
"url": "https://g7.dev/mypage/orders/1",
"data": {
"type": "order_shipped",
"subject": "주문하신 상품이 발송되었습니다",
"body": "주문번호 20260713-0038038875 상품이 발송되었습니다.",
"click_url": "https://g7.dev/mypage/orders/1",
"data": {
"action_url": "https://g7.dev/mypage/orders/1"
}
},
"read_at": "2026-07-13 10:02:33",
"created_at": "2026-07-13 09:58:12"
}
}
```
**에러 응답**
+8 -2
View File
@@ -51,11 +51,17 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "비밀번호가 성공적으로 변경되었습니다.",
"data": null
}
```
**에러 응답**
File diff suppressed because it is too large Load Diff
+241 -8
View File
@@ -38,11 +38,75 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`UserResource::toProfileArray()` 산물)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| uuid | string | `a231747f-e82e-4cf2-9ae1-a261849dce40` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) |
| name | string | `홍길동` | 사용자 이름 |
| nickname | string\|null | `gildong` | 닉네임 (미설정 시 null) |
| email | string | `user@example.com` | 이메일 주소 |
| avatar | string\|null | `null` | 아바타 이미지 URL (`User::getAvatarUrl()` 산물, 미등록 시 null) |
| language | string | `ko` | 사용자 언어 설정 (`config('app.supported_locales')` 값 — 기본 `ko`, `en`) |
| timezone | string\|null | `Asia/Seoul` | 사용자 시간대 식별자 |
| country | string\|null | `KR` | 국가 코드 (ISO 3166-1 alpha-2) |
| status | string | `active` | 계정 상태 (`active`, `inactive`, `blocked`, `withdrawn`, `pending_verification`) |
| status_label | string\|null | `활성` | 상태의 다국어 라벨 (`UserStatus::label()` 산물) |
| status_variant | string\|null | `success` | 상태 배지 색상 키 (`success`/`secondary`/`danger`/`warning`/`info`) |
| homepage | string\|null | `https://example.com` | 홈페이지 URL |
| mobile | string\|null | `010-1234-5678` | 휴대전화 번호 |
| phone | string\|null | `02-123-4567` | 전화번호 |
| zipcode | string\|null | `06234` | 우편번호 |
| address | string\|null | `서울특별시 강남구 테헤란로 1` | 기본 주소 |
| address_detail | string\|null | `101동 202호` | 상세 주소 |
| signature | string\|null | `null` | 게시글 서명 |
| bio | string\|null | `null` | 자기소개 |
| is_super | boolean | `false` | 슈퍼관리자 여부 (`User::isSuperAdmin()` 파생) |
| is_admin | boolean | `false` | 관리자 역할 보유 여부 (`User::isAdmin()` 파생) |
| withdrawn_at | string\|null | `null` | 탈퇴 처리 일시 (사용자 시간대 기준) |
| last_login_at | string\|null | `2026-07-05 19:15:16` | 마지막 로그인 일시 (사용자 시간대 기준) |
| last_login_human | string\|null | `1일 전` | 마지막 로그인 시각의 상대 표현 (`diffForHumans()` 산물) |
| created_at | string\|null | `2026-07-06 19:15:16` | 가입 일시 (사용자 시간대 기준) |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 |
> 위 필드에 더해 `core.user.filter_resource_data` 훅으로 확장 소유 필드가 병합된다 (게시판 모듈 `notify_*`, 마케팅 플러그인 `marketing_consent*`/`email_subscription*`/`third_party_consent*`/`info_disclosure*`/`channels`/`consent_histories`, 이커머스 모듈 `ecommerce_*`). 전체 목록은 `GET /api/me` 문서 참조.
**응답 예시**
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "프로필 정보를 성공적으로 가져왔습니다.",
"data": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "홍길동",
"nickname": "gildong",
"email": "user@example.com",
"avatar": null,
"language": "ko",
"timezone": "Asia/Seoul",
"country": "KR",
"status": "active",
"status_label": "활성",
"status_variant": "success",
"homepage": null,
"mobile": "010-1234-5678",
"phone": null,
"zipcode": "06234",
"address": "서울특별시 강남구 테헤란로 1",
"address_detail": "101동 202호",
"signature": null,
"bio": null,
"is_super": false,
"is_admin": false,
"withdrawn_at": null,
"last_login_at": "2026-07-05 19:15:16",
"last_login_human": "1일 전",
"created_at": "2026-07-06 19:15:16",
"is_owner": true
}
}
```
**에러 응답**
@@ -133,11 +197,83 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`UserResource::toArray()` 산물 — `show` 의 `toProfileArray()` 와 필드 구성이 다르다)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| uuid | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 외부 노출용 UUID |
| name | string | `홍길동` | 사용자 이름 |
| nickname | string\|null | `gildong` | 닉네임 |
| email | string | `user@example.com` | 이메일 주소 |
| avatar | string\|null | `null` | 아바타 이미지 URL (미등록 시 null) |
| language | string | `ko` | 사용자 언어 설정 |
| language_label | string\|null | `한국어` | 언어의 다국어 라벨 (`user.language.{code}` 번역) |
| country | string\|null | `KR` | 국가 코드 (ISO 3166-1 alpha-2) |
| status | string | `active` | 계정 상태 (`active`, `inactive`, `blocked`, `withdrawn`, `pending_verification`) |
| status_label | string\|null | `활성` | 상태의 다국어 라벨 |
| status_variant | string\|null | `success` | 상태 배지 색상 키 (`success`/`secondary`/`danger`/`warning`/`info`) |
| is_admin | boolean | `false` | 관리자 역할 보유 여부 |
| homepage | string\|null | `https://example.com` | 홈페이지 URL |
| mobile | string\|null | `010-1234-5678` | 휴대전화 번호 |
| phone | string\|null | `02-123-4567` | 전화번호 |
| zipcode | string\|null | `06234` | 우편번호 |
| address | string\|null | `서울특별시 강남구 테헤란로 1` | 기본 주소 |
| address_detail | string\|null | `101동 202호` | 상세 주소 |
| signature | string\|null | `null` | 게시글 서명 |
| bio | string\|null | `null` | 자기소개 |
| last_login_at | string\|null | `2026-07-05 19:15:16` | 마지막 로그인 일시 (사용자 시간대 기준) |
| email_verified_at | string\|null | `null` | 이메일 인증 완료 일시 (미인증 시 null) |
| timezone | string\|null | `Asia/Seoul` | 사용자 시간대 식별자 |
| created_at | string\|null | `2026-07-06 19:15:16` | 가입 일시 |
| updated_at | string\|null | `2026-07-08 10:41:24` | 최종 수정 일시 |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 |
| abilities | object | `{"can_read":true,"can_update":true,...}` | 현재 사용자의 리소스 권한 맵 (`can_read`/`can_create`/`can_update`/`can_delete`/`can_assign_roles`) |
> `modules_count`/`plugins_count`/`menus_count`·`modules`/`plugins`/`menus`/`roles`/`permissions`/`consents`/`terms_consent`/`privacy_consent` 는 해당 관계가 로드된 경우에만 포함된다 (프로필 수정 응답에서는 로드되지 않아 생략). 확장 소유 필드는 `core.user.filter_resource_data` 훅으로 병합된다.
**응답 예시**
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "사용자 정보가 성공적으로 업데이트되었습니다.",
"data": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "홍길동",
"nickname": "gildong",
"email": "user@example.com",
"avatar": null,
"language": "ko",
"language_label": "한국어",
"country": "KR",
"status": "active",
"status_label": "활성",
"status_variant": "success",
"is_admin": false,
"homepage": null,
"mobile": "010-1234-5678",
"phone": null,
"zipcode": "06234",
"address": "서울특별시 강남구 테헤란로 1",
"address_detail": "101동 202호",
"signature": null,
"bio": null,
"last_login_at": "2026-07-05 19:15:16",
"email_verified_at": null,
"timezone": "Asia/Seoul",
"created_at": "2026-07-06 19:15:16",
"updated_at": "2026-07-08 10:41:24",
"is_owner": true,
"abilities": {
"can_read": false,
"can_create": false,
"can_update": false,
"can_delete": false,
"can_assign_roles": false
}
}
}
```
**에러 응답**
@@ -174,11 +310,38 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체에 `activities` 배열이 담긴다 (페이지네이션 없음, 최신순 최대 50건)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| activities | array | `[{...}]` | 최근 활동 로그 목록 (최신순, 최대 50건) |
| activities[].id | integer | `1` | 활동 로그 기본 키 |
| activities[].action | string | `profile.update` | 활동 액션 키 (점 표기, 예: `profile.show`, `profile.update`, `auth.login`) |
| activities[].action_label | string | `프로필 수정` | 액션의 다국어 라벨 (`activity_log.action.*` 번역, 확장 액션은 확장 lang 우선) |
| activities[].description | string | `프로필 정보를 수정했습니다.` | 활동 설명 (현재 로케일 기준 렌더 결과, `description_key` 미설정 시 빈 문자열) |
| activities[].ip_address | string\|null | `127.0.0.1` | 활동을 수행한 클라이언트 IP |
| activities[].created_at | string\|null | `2026-07-06T19:15:16+09:00` | 활동 발생 일시 (ISO 8601) |
**응답 예시**
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "활동 기록을 성공적으로 가져왔습니다.",
"data": {
"activities": [
{
"id": 1024,
"action": "profile.update",
"action_label": "프로필 수정",
"description": "프로필 정보를 수정했습니다.",
"ip_address": "127.0.0.1",
"created_at": "2026-07-06T19:15:16+09:00"
}
]
}
}
```
**에러 응답**
@@ -214,11 +377,81 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`UserResource::toArray()` 산물 — `PUT /api/user/profile` 응답과 동일 구조)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| uuid | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 외부 노출용 UUID |
| name | string | `홍길동` | 사용자 이름 |
| nickname | string\|null | `gildong` | 닉네임 |
| email | string | `user@example.com` | 이메일 주소 |
| avatar | string\|null | `null` | 아바타 이미지 URL |
| language | string | `en` | 변경된 사용자 언어 설정 |
| language_label | string\|null | `English` | 언어의 다국어 라벨 (`user.language.{code}` 번역) |
| country | string\|null | `KR` | 국가 코드 (ISO 3166-1 alpha-2) |
| status | string | `active` | 계정 상태 |
| status_label | string\|null | `활성` | 상태의 다국어 라벨 |
| status_variant | string\|null | `success` | 상태 배지 색상 키 |
| is_admin | boolean | `false` | 관리자 역할 보유 여부 |
| homepage | string\|null | `null` | 홈페이지 URL |
| mobile | string\|null | `010-1234-5678` | 휴대전화 번호 |
| phone | string\|null | `null` | 전화번호 |
| zipcode | string\|null | `06234` | 우편번호 |
| address | string\|null | `서울특별시 강남구 테헤란로 1` | 기본 주소 |
| address_detail | string\|null | `101동 202호` | 상세 주소 |
| signature | string\|null | `null` | 게시글 서명 |
| bio | string\|null | `null` | 자기소개 |
| last_login_at | string\|null | `2026-07-05 19:15:16` | 마지막 로그인 일시 |
| email_verified_at | string\|null | `null` | 이메일 인증 완료 일시 |
| timezone | string\|null | `Asia/Seoul` | 사용자 시간대 식별자 |
| created_at | string\|null | `2026-07-06 19:15:16` | 가입 일시 |
| updated_at | string\|null | `2026-07-08 10:41:24` | 최종 수정 일시 |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 |
| abilities | object | `{"can_read":false,...}` | 현재 사용자의 리소스 권한 맵 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "언어 설정이 성공적으로 변경되었습니다.",
"data": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "홍길동",
"nickname": "gildong",
"email": "user@example.com",
"avatar": null,
"language": "en",
"language_label": "English",
"country": "KR",
"status": "active",
"status_label": "활성",
"status_variant": "success",
"is_admin": false,
"homepage": null,
"mobile": "010-1234-5678",
"phone": null,
"zipcode": "06234",
"address": "서울특별시 강남구 테헤란로 1",
"address_detail": "101동 202호",
"signature": null,
"bio": null,
"last_login_at": "2026-07-05 19:15:16",
"email_verified_at": null,
"timezone": "Asia/Seoul",
"created_at": "2026-07-06 19:15:16",
"updated_at": "2026-07-08 10:41:24",
"is_owner": true,
"abilities": {
"can_read": false,
"can_create": false,
"can_update": false,
"can_delete": false,
"can_assign_roles": false
}
}
}
```
**에러 응답**
+1 -1
View File
@@ -440,7 +440,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — 컨트롤러가 `role.delete_success` 키로 `success()` 만 호출)._
**응답 예시**
+89 -15
View File
@@ -319,11 +319,27 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| deleted_count | integer | `3` | 실제로 삭제된 스케줄 건수 (`ScheduleService::bulkDelete` 반환값) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "선택한 스케줄이 삭제되었습니다.",
"data": {
"deleted_count": 3
}
}
```
**에러 응답**
@@ -372,11 +388,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| updated_count | integer | `3` | 상태가 실제로 변경된 스케줄 건수 (`ScheduleService::bulkUpdateStatus` 반환값) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "선택한 스케줄의 상태가 변경되었습니다.",
"data": {
"updated_count": 3
}
}
```
**에러 응답**
@@ -414,11 +446,21 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — 컨트롤러가 `success('schedule.history_delete_success')` 를 데이터 없이 호출)._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "실행 이력이 삭제되었습니다.",
"data": null
}
```
**에러 응답**
@@ -426,7 +468,7 @@ Authorization: Bearer {YOUR_TOKEN}
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.schedules.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | 지정한 `historyId` 의 실행 이력이 존재하지 않는 경우 (`schedule.history_not_found`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
<!-- @generated:end -->
@@ -525,9 +567,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — 컨트롤러가 `success('schedule.delete_success')` 를 데이터 없이 호출)._
**응답 예시**
@@ -950,7 +990,25 @@ Authorization: Bearer {YOUR_TOKEN}
_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
목록 항목은 `ScheduleHistoryResource::toListArray()` 산물입니다 (단건 실행 응답과 달리 `output`/`error_output`/`memory_usage`(raw)/`created_at`/`updated_at` 은 포함되지 않습니다).
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `7` | 실행 이력의 기본 키 (내부 식별자) |
| schedule_id | integer | `1` | 이 이력이 속한 스케줄의 식별자 |
| started_at | string\|null | `2026-07-08T12:14:49+09:00` | 실행 시작 일시 (사용자 타임존 포맷) |
| ended_at | string\|null | `2026-07-08T12:14:49+09:00` | 실행 종료 일시 (사용자 타임존 포맷, 실행 중이면 null) |
| duration | integer\|null | `0` | 실행 소요 시간 |
| duration_formatted | string\|null | `45초` | `duration` 값의 표시용 포맷 문자열 (모델 접근자 파생, 없으면 null) |
| status | string | `success` | 실행 결과 상태: success(성공), failed(실패), running(실행중) |
| status_label | string\|null | `성공` | `status` 값의 사람이 읽는 라벨 (`schedule.result.*` 현지화) |
| exit_code | integer\|null | `0` | 실행 종료 코드 (0=성공, 그 외=실패) |
| memory_usage_formatted | string\|null | `417.34 KB` | 실행 중 메모리 사용량의 표시용 포맷 문자열 |
| trigger_type | string | `manual` | 실행 방식: scheduled(예약 자동 실행), manual(수동 즉시 실행) |
| trigger_type_label | string\|null | `수동 실행` | `trigger_type` 값의 사람이 읽는 라벨 (`schedule.trigger_type.*` 현지화) |
| triggered_by | object\|null | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"관리자"}` | 수동 실행을 트리거한 사용자 정보 (uuid/name — triggeredBy 관계가 로드되지 않았거나 자동 실행이면 null) |
`data.pagination` 은 `current_page`/`last_page`/`per_page`/`total`/`from`/`to`/`has_more_pages` 로 구성됩니다.
**응답 예시**
@@ -963,14 +1021,30 @@ HTTP/1.1 200
"success": true,
"message": "실행 이력을 조회했습니다.",
"data": {
"data": [],
"data": [
{
"id": 7,
"schedule_id": 1,
"started_at": "2026-07-08T12:14:49+09:00",
"ended_at": "2026-07-08T12:14:49+09:00",
"duration": 0,
"duration_formatted": null,
"status": "success",
"status_label": "성공",
"exit_code": 0,
"memory_usage_formatted": "417.34 KB",
"trigger_type": "manual",
"trigger_type_label": "수동 실행",
"triggered_by": null
}
],
"pagination": {
"current_page": 1,
"last_page": 1,
"per_page": 25,
"total": 0,
"from": null,
"to": null,
"total": 1,
"from": 1,
"to": 1,
"has_more_pages": false
}
}
+42 -6
View File
@@ -54,7 +54,7 @@ HTTP/1.1 200
```json
{
"success": true,
"message": "messages.success",
"message": "성공적으로 처리되었습니다.",
"data": {
"urls": [],
"count": 0
@@ -106,11 +106,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| cleared | integer \| string | `12` / `"all"` | 삭제 결과. `layout` 을 지정한 경우 해당 레이아웃에서 무효화된 캐시 항목 수(정수), 미지정 시 전체 캐시를 삭제했음을 뜻하는 문자열 `"all"`. |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"cleared": "all"
}
}
```
**에러 응답**
@@ -119,6 +135,7 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 캐시 무효화(`invalidateByLayout` / `clearAll`) 중 예외가 발생한 경우 (`messages.error_occurred`) |
<!-- @generated:end -->
@@ -315,7 +332,7 @@ HTTP/1.1 200
```json
{
"success": true,
"message": "messages.success",
"message": "성공적으로 처리되었습니다.",
"data": {
"overall": {
"total_entries": 0,
@@ -365,11 +382,29 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| status | string | `dispatched` | 워밍업 요청 접수 상태. 현재 구현에서는 항상 `dispatched` 고정값이며, 실제 사전 렌더가 완료되었음을 뜻하지는 않는다. |
| message | string | `SEO 캐시 워밍업이 시작되었습니다.` | 관리자에게 노출할 안내 문구 (`seo.warmup_dispatched` 다국어 키). |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"status": "dispatched",
"message": "SEO 캐시 워밍업이 시작되었습니다."
}
}
```
**에러 응답**
@@ -377,6 +412,7 @@ Authorization: Bearer {YOUR_TOKEN}
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
| 500 | Internal Server Error | 워밍업 처리 중 예외가 발생한 경우 (`messages.error_occurred`) |
<!-- @generated:end -->
+303 -128
View File
@@ -171,111 +171,111 @@ HTTP/1.1 200
| advanced | body | array | 아니오 | — | 고급 탭 설정 묶음 (캐시·디버그·코어 업데이트·GeoIP 설정) |
| notifications | body | array | 아니오 | — | 알림 탭 설정 묶음. channels 배열로 각 알림 채널의 id·is_active(활성 여부)·sort_order(표시 순서)를 저장 |
| identity | body | array | 아니오 | — | 본인인증(IDV) 탭 설정 묶음 (기본 provider·목적별 provider 매핑·챌린지 유효시간·최대 시도 횟수) |
| notifications.channels | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| notifications.channels | body | array | 아니오 | — | 알림 채널 목록. 각 원소는 `id`(채널 식별자, 필수·최대 50자)·`is_active`(활성 여부, 필수 boolean)·`sort_order`(표시 순서, 0 이상 정수) 로 구성 |
| general.site_name | body | string | 예 | max 100 | general.site 이름 (식별자) |
| general.site_url | body | string | 예 | max 255 | <!-- TODO: 용도 --> |
| general.site_description | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| general.admin_email | body | email | 예 | max 255 | <!-- TODO: 용도 --> |
| general.timezone | body | string | 예 | — | <!-- TODO: 용도 --> |
| general.language | body | string | 예 | — | <!-- TODO: 용도 --> |
| general.currency | body | string | 아니오 | max 10 | <!-- TODO: 용도 --> |
| general.maintenance_mode | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| general.site_logo | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| mail.mailer | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| mail.host | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| mail.port | body | integer | 아니오 | min 1, max 65535 | <!-- TODO: 용도 --> |
| mail.username | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| mail.password | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| mail.encryption | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| mail.mailgun_domain | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| mail.mailgun_secret | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| mail.mailgun_endpoint | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| mail.ses_key | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| mail.ses_secret | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| mail.ses_region | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| mail.from_address | body | email | 아니오 | max 255 | <!-- TODO: 용도 --> |
| general.site_url | body | string | 예 | max 255 | 사이트 기본 URL |
| general.site_description | body | string | 아니오 | max 500 | 사이트 설명 |
| general.admin_email | body | email | 예 | max 255 | 관리자 이메일 주소 |
| general.timezone | body | string | 예 | — | 타임존 식별자 |
| general.language | body | string | 예 | — | 언어 코드 |
| general.currency | body | string | 아니오 | max 10 | 통화 코드 (ISO 4217 — 예: KRW) |
| general.maintenance_mode | body | boolean | 아니오 | — | 점검 모드 사용 여부 (사이트 접근 차단) |
| general.site_logo | body | array | 아니오 | — | 사이트 로고 이미지 |
| mail.mailer | body | string | 아니오 | — | 메일 발송 드라이버 (smtp/mailgun/ses) |
| mail.host | body | string | 아니오 | max 255 | 호스트 주소 |
| mail.port | body | integer | 아니오 | min 1, max 65535 | 포트 번호 |
| mail.username | body | string | 아니오 | max 255 | 사용자명 (로그인/인증 아이디) |
| mail.password | body | string | 아니오 | max 255 | 비밀번호 |
| mail.encryption | body | string | 아니오 | — | 전송 암호화 방식 (tls/ssl) |
| mail.mailgun_domain | body | string | 아니오 | max 255 | Mailgun 도메인 |
| mail.mailgun_secret | body | string | 아니오 | max 255 | Mailgun 시크릿 키 |
| mail.mailgun_endpoint | body | string | 아니오 | max 255 | Mailgun 엔드포인트 |
| mail.ses_key | body | string | 아니오 | max 255 | SES 액세스 키 |
| mail.ses_secret | body | string | 아니오 | max 255 | SES 시크릿 키 |
| mail.ses_region | body | string | 아니오 | max 255 | SES 리전 |
| mail.from_address | body | email | 아니오 | max 255 | 발신자 주소 |
| mail.from_name | body | string | 아니오 | max 255 | mail.from 이름 (식별자) |
| upload.max_file_size | body | integer | 아니오 | min 1, max 1024 | <!-- TODO: 용도 --> |
| upload.allowed_extensions | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| upload.image_max_width | body | integer | 아니오 | min 100, max 10000 | <!-- TODO: 용도 --> |
| upload.image_max_height | body | integer | 아니오 | min 100, max 10000 | <!-- TODO: 용도 --> |
| upload.image_quality | body | integer | 아니오 | min 1, max 100 | <!-- TODO: 용도 --> |
| seo.meta_title_suffix | body | string | 아니오 | max 100 | <!-- TODO: 용도 --> |
| seo.meta_description | body | string | 아니오 | max 160 | <!-- TODO: 용도 --> |
| seo.meta_keywords | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| upload.max_file_size | body | integer | 아니오 | min 1, max 1024 | 업로드 허용 최대 파일 크기 |
| upload.allowed_extensions | body | string | 아니오 | — | 업로드 허용 확장자 목록 |
| upload.image_max_width | body | integer | 아니오 | min 100, max 10000 | 이미지 리사이즈 최대 너비 (px) |
| upload.image_max_height | body | integer | 아니오 | min 100, max 10000 | 이미지 리사이즈 최대 높이 (px) |
| upload.image_quality | body | integer | 아니오 | min 1, max 100 | 이미지 리사이즈 시 압축 품질 (1~100) |
| seo.meta_title_suffix | body | string | 아니오 | max 100 | 모든 페이지 SEO 제목 뒤에 붙는 접미 문구 |
| seo.meta_description | body | string | 아니오 | max 160 | SEO 메타 설명 (검색엔진/소셜 공유 표시 요약) |
| seo.meta_keywords | body | string | 아니오 | max 255 | SEO 메타 키워드 (검색엔진 노출 키워드, 쉼표 구분) |
| seo.google_analytics_id | body | string | 아니오 | max 50 | seo.google analytics 식별자 |
| seo.google_site_verification | body | string | 아니오 | max 100 | <!-- TODO: 용도 --> |
| seo.naver_site_verification | body | string | 아니오 | max 100 | <!-- TODO: 용도 --> |
| seo.bot_user_agents | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| seo.bot_detection_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| seo.bot_detection_library_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| seo.google_site_verification | body | string | 아니오 | max 100 | Google Search Console 사이트 소유 확인 코드 |
| seo.naver_site_verification | body | string | 아니오 | max 100 | 네이버 서치어드바이저 사이트 소유 확인 코드 |
| seo.bot_user_agents | body | array | 아니오 | — | 봇으로 판정할 User-Agent 목록 |
| seo.bot_detection_enabled | body | boolean | 아니오 | — | 검색엔진 봇 감지 사용 여부 (봇 요청에 SEO 렌더링 적용) |
| seo.bot_detection_library_enabled | body | boolean | 아니오 | — | 봇 감지 라이브러리 사용 여부 (User-Agent 목록 대신 라이브러리 판정) |
| seo.og_default_site_name | body | string | 아니오 | max 200 | seo.og default site 이름 (식별자) |
| seo.og_image_default_width | body | integer | 아니오 | min 0, max 8000 | <!-- TODO: 용도 --> |
| seo.og_image_default_height | body | integer | 아니오 | min 0, max 8000 | <!-- TODO: 용도 --> |
| seo.twitter_default_card | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| seo.twitter_default_site | body | string | 아니오 | max 50 | <!-- TODO: 용도 --> |
| seo.cache_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| seo.cache_ttl | body | integer | 아니오 | min 60, max 86400 | <!-- TODO: 용도 --> |
| seo.sitemap_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| seo.sitemap_cache_ttl | body | integer | 아니오 | min 3600, max 604800 | <!-- TODO: 용도 --> |
| seo.sitemap_schedule | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| seo.sitemap_schedule_time | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| seo.generator_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| seo.generator_content | body | string | 아니오 | max 200 | <!-- TODO: 용도 --> |
| security.force_https | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| security.login_attempt_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| security.auth_token_lifetime | body | integer | 아니오 | min 0, max 3600 | <!-- TODO: 용도 --> |
| security.max_login_attempts | body | integer | 아니오 | min 0, max 100 | <!-- TODO: 용도 --> |
| security.login_lockout_time | body | integer | 아니오 | min 0, max 1440 | <!-- TODO: 용도 --> |
| advanced.cache_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| advanced.layout_cache_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| advanced.layout_cache_ttl | body | integer | 아니오 | min 0, max 14400 | <!-- TODO: 용도 --> |
| advanced.stats_cache_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| advanced.stats_cache_ttl | body | integer | 아니오 | min 0, max 14400 | <!-- TODO: 용도 --> |
| advanced.seo_cache_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| advanced.seo_cache_ttl | body | integer | 아니오 | min 0, max 14400 | <!-- TODO: 용도 --> |
| advanced.debug_mode | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| advanced.sql_query_log | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| advanced.core_update_github_url | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| advanced.core_update_github_token | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| advanced.geoip_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| advanced.geoip_license_key | body | string | 아니오 | max 200 | <!-- TODO: 용도 --> |
| advanced.geoip_auto_update_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| drivers.storage_driver | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| drivers.s3_bucket | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| drivers.s3_region | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| drivers.s3_access_key | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| drivers.s3_secret_key | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| drivers.s3_url | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| drivers.cache_driver | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| drivers.redis_host | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| drivers.redis_port | body | integer | 아니오 | min 1, max 65535 | <!-- TODO: 용도 --> |
| drivers.redis_password | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| drivers.redis_database | body | integer | 아니오 | min 0, max 15 | <!-- TODO: 용도 --> |
| drivers.memcached_host | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| drivers.memcached_port | body | integer | 아니오 | min 1, max 65535 | <!-- TODO: 용도 --> |
| drivers.session_driver | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| drivers.session_lifetime | body | integer | 아니오 | min 1, max 43200 | <!-- TODO: 용도 --> |
| drivers.queue_driver | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| drivers.websocket_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| seo.og_image_default_width | body | integer | 아니오 | min 0, max 8000 | 기본 Open Graph 이미지 너비 (px) |
| seo.og_image_default_height | body | integer | 아니오 | min 0, max 8000 | 기본 Open Graph 이미지 높이 (px) |
| seo.twitter_default_card | body | string | 아니오 | — | 기본 트위터 카드 유형 (summary 등) |
| seo.twitter_default_site | body | string | 아니오 | max 50 | 기본 트위터 사이트 계정 (@handle) |
| seo.cache_enabled | body | boolean | 아니오 | — | 캐시 사용 여부 |
| seo.cache_ttl | body | integer | 아니오 | min 60, max 86400 | 캐시 유효 시간 (초) |
| seo.sitemap_enabled | body | boolean | 아니오 | — | sitemap.xml 생성 사용 여부 |
| seo.sitemap_cache_ttl | body | integer | 아니오 | min 3600, max 604800 | sitemap 캐시 유효 시간 (초) |
| seo.sitemap_schedule | body | string | 아니오 | — | sitemap 자동 생성 주기 |
| seo.sitemap_schedule_time | body | string | 아니오 | — | sitemap 자동 생성 시각 |
| seo.generator_enabled | body | boolean | 아니오 | — | SEO 페이지 생성기 사용 여부 |
| seo.generator_content | body | string | 아니오 | max 200 | SEO 렌더링 본문 생성 방식 |
| security.force_https | body | boolean | 아니오 | — | HTTPS 강제 리다이렉트 여부 |
| security.login_attempt_enabled | body | boolean | 아니오 | — | 로그인 시도 제한 사용 여부 |
| security.auth_token_lifetime | body | integer | 아니오 | min 0, max 3600 | 인증 토큰 유효 시간 (분) |
| security.max_login_attempts | body | integer | 아니오 | min 0, max 100 | 로그인 실패 허용 횟수 (초과 시 잠금) |
| security.login_lockout_time | body | integer | 아니오 | min 0, max 1440 | 로그인 잠금 지속 시간 (분) |
| advanced.cache_enabled | body | boolean | 아니오 | — | 캐시 사용 여부 |
| advanced.layout_cache_enabled | body | boolean | 아니오 | — | 레이아웃 캐시 사용 여부 (레이아웃 데이터를 캐시) |
| advanced.layout_cache_ttl | body | integer | 아니오 | min 0, max 14400 | 레이아웃 캐시 만료 시간 (초, 0 = 만료 없음) |
| advanced.stats_cache_enabled | body | boolean | 아니오 | — | 통계 캐시 사용 여부 (대시보드 통계를 캐시) |
| advanced.stats_cache_ttl | body | integer | 아니오 | min 0, max 14400 | 통계 캐시 만료 시간 (초, 0 = 만료 없음) |
| advanced.seo_cache_enabled | body | boolean | 아니오 | — | SEO 캐시 사용 여부 (SEO 메타데이터를 캐시) |
| advanced.seo_cache_ttl | body | integer | 아니오 | min 0, max 14400 | SEO 캐시 만료 시간 (초, 0 = 만료 없음) |
| advanced.debug_mode | body | boolean | 아니오 | — | 디버그 모드 사용 여부 (상세 오류 노출) |
| advanced.sql_query_log | body | boolean | 아니오 | — | SQL 쿼리 로그 기록 여부 |
| advanced.core_update_github_url | body | string | 아니오 | max 500 | 코어 업데이트를 확인할 GitHub 저장소 URL |
| advanced.core_update_github_token | body | string | 아니오 | max 500 | 프라이빗 저장소의 코어/확장 업데이트에 사용할 GitHub 액세스 토큰 (공개 저장소는 비워둘 수 있음) |
| advanced.geoip_enabled | body | boolean | 아니오 | — | IP 기반 타임존 감지(GeoIP) 사용 여부 |
| advanced.geoip_license_key | body | string | 아니오 | max 200 | MaxMind GeoLite2 라이선스 키 |
| advanced.geoip_auto_update_enabled | body | boolean | 아니오 | — | GeoIP DB 자동 업데이트 사용 여부 (주 1회 자동 재다운로드) |
| drivers.storage_driver | body | string | 아니오 | — | 스토리지 드라이버 (local/s3) |
| drivers.s3_bucket | body | string | 아니오 | max 255 | S3 버킷명 |
| drivers.s3_region | body | string | 아니오 | — | S3 리전 |
| drivers.s3_access_key | body | string | 아니오 | max 255 | S3 액세스 키 |
| drivers.s3_secret_key | body | string | 아니오 | max 255 | S3 시크릿 키 |
| drivers.s3_url | body | string | 아니오 | max 500 | S3 엔드포인트 URL |
| drivers.cache_driver | body | string | 아니오 | — | 캐시 드라이버 (file/redis/memcached) |
| drivers.redis_host | body | string | 아니오 | max 255 | Redis 호스트 주소 |
| drivers.redis_port | body | integer | 아니오 | min 1, max 65535 | Redis 포트 번호 |
| drivers.redis_password | body | string | 아니오 | max 255 | Redis 비밀번호 |
| drivers.redis_database | body | integer | 아니오 | min 0, max 15 | Redis 데이터베이스 번호 |
| drivers.memcached_host | body | string | 아니오 | max 255 | Memcached 호스트 주소 |
| drivers.memcached_port | body | integer | 아니오 | min 1, max 65535 | Memcached 포트 번호 |
| drivers.session_driver | body | string | 아니오 | — | 세션 드라이버 (file/database/redis) |
| drivers.session_lifetime | body | integer | 아니오 | min 1, max 43200 | 세션 유효 시간 (분) |
| drivers.queue_driver | body | string | 아니오 | — | 큐 드라이버 (sync/database/redis) |
| drivers.websocket_enabled | body | boolean | 아니오 | — | WebSocket 사용 여부 |
| drivers.websocket_app_id | body | string | 아니오 | max 255 | drivers.websocket app 식별자 |
| drivers.websocket_app_key | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| drivers.websocket_app_secret | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| drivers.websocket_host | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| drivers.websocket_port | body | integer | 아니오 | min 1, max 65535 | <!-- TODO: 용도 --> |
| drivers.websocket_scheme | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| drivers.websocket_verify_ssl | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| drivers.websocket_server_host | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| drivers.websocket_server_port | body | integer | 아니오 | min 1, max 65535 | <!-- TODO: 용도 --> |
| drivers.websocket_server_scheme | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| drivers.search_engine_driver | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| drivers.log_driver | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| drivers.log_level | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| drivers.log_days | body | integer | 아니오 | min 1, max 365 | <!-- TODO: 용도 --> |
| identity.default_provider | body | string | 아니오 | max 100 | <!-- TODO: 용도 --> |
| identity.purpose_providers | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| identity.challenge_ttl_minutes | body | integer | 아니오 | min 1, max 1440 | <!-- TODO: 용도 --> |
| identity.max_attempts | body | integer | 아니오 | min 1, max 20 | <!-- TODO: 용도 --> |
| drivers.websocket_app_key | body | string | 아니오 | max 255 | WebSocket 앱 키 |
| drivers.websocket_app_secret | body | string | 아니오 | max 255 | WebSocket 앱 시크릿 |
| drivers.websocket_host | body | string | 아니오 | max 255 | WebSocket 호스트 주소 |
| drivers.websocket_port | body | integer | 아니오 | min 1, max 65535 | WebSocket 포트 번호 |
| drivers.websocket_scheme | body | string | 아니오 | — | WebSocket 스킴 (http/https) |
| drivers.websocket_verify_ssl | body | boolean | 아니오 | — | WebSocket 서버 SSL 인증서 검증 여부 |
| drivers.websocket_server_host | body | string | 아니오 | max 255 | WebSocket 서버 호스트 주소 (서버측 발행 대상) |
| drivers.websocket_server_port | body | integer | 아니오 | min 1, max 65535 | WebSocket 서버 포트 번호 (서버측 발행 대상) |
| drivers.websocket_server_scheme | body | string | 아니오 | — | WebSocket 서버 스킴 (http/https — 서버측 발행 대상) |
| drivers.search_engine_driver | body | string | 아니오 | — | 검색 엔진 드라이버 (Scout 엔진 선택) |
| drivers.log_driver | body | string | 아니오 | — | 로그 드라이버 (single/daily/stack) |
| drivers.log_level | body | string | 아니오 | — | 로그 레벨 (debug/info/warning/error 등) |
| drivers.log_days | body | integer | 아니오 | min 1, max 365 | 로그 파일 보관 일수 |
| identity.default_provider | body | string | 아니오 | max 100 | 본인인증 기본 프로바이더 (목적별로 지정되지 않은 경우 사용). 예: `g7:core.mail` |
| identity.purpose_providers | body | array | 아니오 | — | 본인인증 목적(Purpose)별 프로바이더 매핑. 미지정 목적은 기본 프로바이더를 사용 |
| identity.challenge_ttl_minutes | body | integer | 아니오 | min 1, max 1440 | 발급된 인증 코드/링크(challenge)의 유효시간 (분) |
| identity.max_attempts | body | integer | 아니오 | min 1, max 20 | challenge 최대 시도 횟수 (연속 실패 시 잠금 — 재전송 필요) |
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.save_validation_rules`, `core.search.engine_drivers`).
@@ -435,11 +435,55 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| settings | object | `{"general":{...},"security":{...},"available_drivers":{...}}` | 저장 직후 다시 조회한 전체 설정 집계. 구조는 `GET /api/admin/settings` 의 `data` 와 동일 (general/security/mail/upload/seo/advanced/cache/debug/drivers/core_update/geoip/notifications/identity + `available_drivers`) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "설정이 성공적으로 저장되었습니다.",
"data": {
"settings": {
"general": {
"site_name": "Test Site",
"site_url": "https://test.example.com",
"site_description": "",
"admin_email": "admin@example.com",
"timezone": "Asia/Seoul",
"language": "ko",
"currency": "KRW",
"maintenance_mode": false,
"site_logo": []
},
"security": {
"force_https": true,
"login_attempt_enabled": true,
"auth_token_lifetime": 0,
"max_login_attempts": 5,
"login_lockout_time": 30
},
"available_drivers": {
"storage": [
{
"id": "local",
"label": {
"ko": "로컬",
"en": "Local"
}
}
]
}
}
}
}
```
> 위 예시는 지면상 일부 그룹만 표기한 것으로, 실제 응답의 `data.settings` 에는 `GET /api/admin/settings` 와 동일한 전체 그룹이 포함됩니다.
**에러 응답**
@@ -534,11 +578,23 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| backup_path | string | `backups/backup_2026-07-14_030000.zip` | 생성된 설정 백업 ZIP 의 저장소 상대 경로 (카테고리별 설정 JSON 을 묶은 아카이브). `POST /api/admin/settings/restore` 의 `backup_path` 로 그대로 전달해 복원에 사용 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "데이터베이스 백업이 성공적으로 시작되었습니다.",
"data": {
"backup_path": "backups/backup_2026-07-14_030000.zip"
}
}
```
**에러 응답**
@@ -546,6 +602,7 @@ Authorization: Bearer {YOUR_TOKEN}
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
| 500 | Internal Server Error | 백업 파일 생성에 실패한 경우 (`settings.backup_failed`) |
<!-- @generated:end -->
@@ -575,18 +632,26 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만). 컨트롤러가 `ResponseHelper::success('settings.backup_success')` 를 데이터 없이 호출하므로 `data` 는 `null` 입니다._
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "데이터베이스 백업이 성공적으로 시작되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 백업이 수행되지 않은 경우 (`settings.backup_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
| 500 | Internal Server Error | 백업 처리 중 예외가 발생한 경우 (`settings.backup_error`) |
<!-- @generated:end -->
@@ -616,18 +681,26 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만). 컨트롤러가 `ResponseHelper::success('settings.cache_clear_success')` 를 데이터 없이 호출하므로 `data` 는 `null` 입니다._
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "캐시가 성공적으로 정리되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 캐시 정리가 수행되지 않은 경우 (`settings.cache_clear_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
| 500 | Internal Server Error | 캐시 정리 중 예외가 발생한 경우 (`settings.cache_clear_error`) |
<!-- @generated:end -->
@@ -657,18 +730,38 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (GeoIpDatabaseService::updateDatabase() 의 `data`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| database_path | string | `storage/app/geoip/GeoLite2-City.mmdb` | 갱신된 GeoLite2 DB 파일의 절대 경로 (`config('geoip.database_path')`) |
| file_size_bytes | integer | `60000000` | 다운로드·전개 후 최종 DB 파일 크기 (바이트) |
| elapsed_seconds | number | `12.34` | 다운로드부터 교체 완료까지 소요 시간 (초) |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "settings.geoip.update_success",
"data": {
"database_path": "storage/app/geoip/GeoLite2-City.mmdb",
"file_size_bytes": 60000000,
"elapsed_seconds": 12.34
}
}
```
> `message` 는 컨트롤러가 넘기는 `settings.geoip.update_success` 키를 번역한 값입니다. 현재 코어 `lang/{ko,en}/settings.php` 에 `geoip.*` 항목이 정의되어 있지 않아 번역이 없으면 키 문자열이 그대로 내려갑니다 (관리자 화면은 프론트엔드 다국어 키로 별도 문구를 표시).
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 400 | Bad Request | MaxMind 라이선스 키가 설정되지 않은 경우 (`missing_license_key`) |
| 401 | Unauthorized | MaxMind 라이선스 키가 유효하지 않은 경우 (`unauthorized`) |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
| 500 | Internal Server Error | MaxMind 연결 실패(`connection_failed`) 또는 다운로드·압축 해제 실패 |
<!-- @generated:end -->
@@ -698,9 +791,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만). 컨트롤러가 `ResponseHelper::success('settings.optimize_success')` 를 데이터 없이 호출하므로 `data` 는 `null` 입니다._
**응답 예시**
@@ -760,19 +851,32 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| app_key | string | `base64:97gZH********************************` | 재생성된 애플리케이션 키를 마스킹한 문자열 (앞 12자 + 별표 32개). 전체 원문 키는 반환하지 않으며 `.env` 의 `APP_KEY` 에만 기록됨 |
**응답 예시**
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "어플리케이션 키가 성공적으로 재생성되었습니다.",
"data": {
"app_key": "base64:97gZH********************************"
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
| 401 | Unauthorized | 본문 `password` 가 요청자 본인의 비밀번호와 일치하지 않는 경우 (`settings.invalid_password`) |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없거나, FormRequest 가 `super_admin` 역할이 아닌 사용자를 거부한 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | `.env` 기록/config 캐시 재생성 실패 (`settings.app_key_regenerate_failed`) |
<!-- @generated:end -->
@@ -811,19 +915,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만). 컨트롤러가 `ResponseHelper::success('settings.restore_success')` 를 데이터 없이 호출하므로 `data` 는 `null` 입니다._
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "설정이 성공적으로 복원되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 복원이 수행되지 않은 경우 (`settings.restore_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 백업 파일을 읽을 수 없는 등 복원 중 예외 발생 (`settings.restore_error`) |
<!-- @generated:end -->
@@ -1047,11 +1159,35 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (DriverConnectionTester::testAll() 산물)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| results | object | `{"redis":{"success":true,"message":"Redis 서버에 성공적으로 연결되었습니다.","latency":"3ms"}}` | 드라이버별 테스트 결과 맵. 키는 실제 테스트가 수행된 드라이버(`s3`/`redis`/`memcached`/`websocket`)만 포함 — 요청 설정에서 해당 드라이버를 쓰지 않으면 키 자체가 없음 |
| results.{driver}.success | boolean | `true` | 해당 드라이버 연결 성공 여부 |
| results.{driver}.message | string | `Redis 서버에 성공적으로 연결되었습니다.` | 결과 메시지 (성공/실패 사유 — 설정 누락·확장 미설치·인증 실패 등) |
| results.{driver}.latency | string | `3ms` | 성공 시에만 포함. 연결 왕복 소요 시간 |
| results.{driver}.error | string | `Connection refused` | 실패 시에만 포함. 원본 예외 메시지 |
| all_passed | boolean | `true` | 수행된 모든 드라이버 테스트가 성공했는지 여부. `false` 여도 HTTP 200 으로 응답하며 message 가 `일부 드라이버 연결 테스트가 실패했습니다.` 로 바뀜 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "모든 드라이버 연결 테스트가 성공했습니다.",
"data": {
"results": {
"redis": {
"success": true,
"message": "Redis 서버에 성공적으로 연결되었습니다.",
"latency": "3ms"
}
},
"all_passed": true
}
}
```
**에러 응답**
@@ -1060,6 +1196,7 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 테스트 실행 중 예외가 발생한 경우 (`settings.driver_test_error`) |
<!-- @generated:end -->
@@ -1126,11 +1263,25 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-500 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (발송 성공 시에만 반환)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| subject | string | `그누보드7 테스트 메일` | 실제로 발송한 테스트 메일의 제목 (`settings.test_mail_subject` — 앱 이름이 치환됨) |
| body | string | `이것은 그누보드7에서 발송한 테스트 메일입니다. 이 메일을 받으셨다면 메일 설정이 올바르게 구성되어 있습니다.` | 실제로 발송한 테스트 메일의 본문 (`settings.test_mail_body`, 평문) |
**응답 예시**
<!-- 실측 제외: http-500 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "테스트 메일이 성공적으로 발송되었습니다.",
"data": {
"subject": "그누보드7 테스트 메일",
"body": "이것은 그누보드7에서 발송한 테스트 메일입니다. 이 메일을 받으셨다면 메일 설정이 올바르게 구성되어 있습니다."
}
}
```
**에러 응답**
@@ -1139,6 +1290,7 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 발송 실패 시 — `message` 는 `테스트 메일 발송에 실패했습니다.`, `error` 에 원본 예외 메시지(SMTP 인증 실패·연결 거부 등)가 담김 |
<!-- @generated:end -->
@@ -1170,11 +1322,25 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| key | string | `general.site_name` | 요청한 설정 키를 그대로 되돌려 준 값 (`{category}.{field}` 형태) |
| value | mixed | `Test Site` | 해당 설정 키의 현재 값. 설정 항목의 자료형에 따라 문자열/정수/불리언/배열이 될 수 있으며, 키가 없으면 `null` |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "설정을 성공적으로 가져왔습니다.",
"data": {
"key": "general.site_name",
"value": "Test Site"
}
}
```
**에러 응답**
@@ -1183,6 +1349,7 @@ Authorization: Bearer {YOUR_TOKEN}
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 500 | Internal Server Error | 설정 조회 중 예외가 발생한 경우 (`settings.fetch_failed`) |
<!-- @generated:end -->
@@ -1222,20 +1389,28 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만). 컨트롤러가 `ResponseHelper::success('settings.update_success')` 를 데이터 없이 호출하므로 `data` 는 `null` 입니다._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "설정이 성공적으로 업데이트되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 저장이 수행되지 않은 경우 (`settings.update_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 저장 중 예외가 발생한 경우 (`settings.update_error`) |
<!-- @generated:end -->
File diff suppressed because it is too large Load Diff
+280 -15
View File
@@ -293,11 +293,85 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (생성된 사용자 — `UserResource::toArray()`, 관계 미로드이므로 modules/plugins/menus/roles/permissions/consents 키는 응답에 포함되지 않음)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| uuid | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) |
| name | string | `API 문서 샘플 사용자` | 사용자 이름 |
| nickname | string\|null | `null` | 닉네임 |
| email | string | `apidoc-sample-user@example.com` | 이메일 주소 |
| avatar | string\|null | `null` | 프로필 아바타 이미지 URL (미설정 시 null) |
| language | string | `ko` | 사용자 언어 설정 (ko: 한국어, en: English) |
| language_label | string | `한국어` | 언어 코드의 현지화 라벨 (`user.language.{code}` 번역) |
| country | string\|null | `KR` | 국가 코드 (ISO 3166-1 alpha-2) |
| status | string | `active` | 계정 상태 (UserStatus Enum: active/inactive/blocked/withdrawn) |
| status_label | string | `활성` | 상태의 사람이 읽는 라벨 (UserStatus::label()) |
| status_variant | string | `success` | 상태 표시 색상/스타일 변형 키 (UserStatus::variant() — UI 배지용) |
| is_admin | boolean | `false` | 관리자 역할 보유 여부 (User::isAdmin()) |
| homepage | string\|null | `null` | 홈페이지 URL |
| mobile | string\|null | `null` | 휴대폰 번호 |
| phone | string\|null | `null` | 전화번호 |
| zipcode | string\|null | `null` | 우편번호 |
| address | string\|null | `null` | 기본 주소 |
| address_detail | string\|null | `null` | 상세 주소 |
| signature | string\|null | `null` | 서명 |
| bio | string\|null | `null` | 자기소개 |
| last_login_at | string\|null | `null` | 마지막 로그인 일시 (신규 생성 시 null) |
| email_verified_at | string\|null | `null` | 이메일 인증 일시 (미인증 시 null) |
| timezone | string\|null | `Asia/Seoul` | 사용자 시간대 |
| created_at | string | `2026-07-08 10:41:24` | 생성 일시 |
| updated_at | string | `2026-07-08 10:41:24` | 최종 수정 일시 |
| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_read":true,"can_create":true,"can_update":true,"can_delete":true,"can_assign_roles":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (슈퍼관리자 대상은 can_delete=false 강제) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "사용자가 성공적으로 생성되었습니다.",
"data": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "예시 이름",
"nickname": null,
"email": "user@example.com",
"avatar": null,
"language": "ko",
"language_label": "한국어",
"country": "KR",
"status": "active",
"status_label": "활성",
"status_variant": "success",
"is_admin": false,
"homepage": null,
"mobile": "010-1234-5678",
"phone": null,
"zipcode": null,
"address": null,
"address_detail": null,
"signature": null,
"bio": null,
"last_login_at": null,
"email_verified_at": null,
"timezone": "Asia/Seoul",
"created_at": "2026-07-08 10:41:24",
"updated_at": "2026-07-08 10:41:24",
"is_owner": false,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
}
}
}
```
**에러 응답**
@@ -306,6 +380,7 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 사용자 생성 중 예외 발생 (`user.create_failed`, `errors.error` 에 예외 메시지) |
<!-- @generated:end -->
@@ -348,11 +423,29 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (UserService::bulkUpdateStatus() 반환값)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| updated_count | integer | `3` | 실제로 상태가 변경된 사용자 수 (요청자 본인 제외 규칙 적용 후 갱신된 행 수) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": ":count명의 사용자 상태가 변경되었습니다.",
"data": {
"updated_count": 3
}
}
```
> `user.bulk_status_updated` 메시지는 `:count` 치환자를 갖지만 컨트롤러가 messageParams 를 넘기지 않으므로 응답 message 에는 치환되지 않은 원문이 그대로 담긴다. 실제 변경 건수는 `data.updated_count` 로 판단한다.
**에러 응답**
@@ -360,7 +453,8 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지 — 예: 요청자 본인 UUID 포함 시 `ExcludeCurrentUser` 위반) |
| 500 | Internal Server Error | 일괄 변경 중 예외 발생 (`user.bulk_update_status_failed`) |
<!-- @generated:end -->
@@ -401,11 +495,29 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| available | boolean | `true` | 해당 이메일의 사용 가능 여부 (true: 사용 가능, false: 이미 사용 중). `exclude_user_id` 로 지정한 사용자가 소유한 이메일이면 true 로 판정 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "사용 가능한 이메일입니다.",
"data": {
"available": true
}
}
```
> 이미 사용 중인 이메일이면 동일한 200 응답에 `message` 가 `이미 사용 중인 이메일입니다.` (`user.email_unavailable`), `data.available` 이 `false` 로 반환된다.
**에러 응답**
@@ -413,7 +525,8 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지 — 이메일 형식/필수, exclude_user_id UUID 형식·존재 여부) |
| 500 | Internal Server Error | 중복 확인 중 예외 발생 (`user.email_check_failed`) |
<!-- @generated:end -->
@@ -695,11 +808,87 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_목록 응답: `data[]` 배열 항목의 필드 (페이지네이션 없는 단순 컬렉션 — `UserResource::collection()`, 관계 미로드이므로 modules/plugins/menus/roles/permissions/consents 키는 포함되지 않음). 일치하는 사용자가 없으면 `data` 는 빈 배열._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| uuid | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) |
| name | string | `API 문서 샘플 사용자` | 사용자 이름 |
| nickname | string\|null | `null` | 닉네임 |
| email | string | `apidoc-sample-user@example.com` | 이메일 주소 |
| avatar | string\|null | `null` | 프로필 아바타 이미지 URL (미설정 시 null) |
| language | string | `ko` | 사용자 언어 설정 (ko: 한국어, en: English) |
| language_label | string | `한국어` | 언어 코드의 현지화 라벨 (`user.language.{code}` 번역) |
| country | string\|null | `KR` | 국가 코드 (ISO 3166-1 alpha-2) |
| status | string | `active` | 계정 상태 (UserStatus Enum: active/inactive/blocked/withdrawn) |
| status_label | string | `활성` | 상태의 사람이 읽는 라벨 (UserStatus::label()) |
| status_variant | string | `success` | 상태 표시 색상/스타일 변형 키 (UserStatus::variant() — UI 배지용) |
| is_admin | boolean | `true` | 관리자 역할 보유 여부 (User::isAdmin()) |
| homepage | string\|null | `null` | 홈페이지 URL |
| mobile | string\|null | `null` | 휴대폰 번호 |
| phone | string\|null | `null` | 전화번호 |
| zipcode | string\|null | `null` | 우편번호 |
| address | string\|null | `null` | 기본 주소 |
| address_detail | string\|null | `null` | 상세 주소 |
| signature | string\|null | `null` | 서명 |
| bio | string\|null | `null` | 자기소개 |
| last_login_at | string\|null | `2026-07-07 10:41:24` | 마지막 로그인 일시 (미로그인 시 null) |
| email_verified_at | string\|null | `2026-07-08 10:41:24` | 이메일 인증 일시 (미인증 시 null) |
| timezone | string\|null | `Asia/Seoul` | 사용자 시간대 |
| created_at | string | `2026-07-08 10:41:24` | 생성 일시 |
| updated_at | string | `2026-07-08 10:41:24` | 최종 수정 일시 |
| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_read":true,"can_create":true,"can_update":true,"can_delete":true,"can_assign_roles":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "사용자 검색을 성공적으로 완료했습니다.",
"data": [
{
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"nickname": null,
"email": "apidoc-sample-user@example.com",
"avatar": null,
"language": "ko",
"language_label": "한국어",
"country": "KR",
"status": "active",
"status_label": "활성",
"status_variant": "success",
"is_admin": true,
"homepage": null,
"mobile": null,
"phone": null,
"zipcode": null,
"address": null,
"address_detail": null,
"signature": null,
"bio": null,
"last_login_at": "2026-07-07 10:41:24",
"email_verified_at": "2026-07-08 10:41:24",
"timezone": "Asia/Seoul",
"created_at": "2026-07-08 10:41:24",
"updated_at": "2026-07-08 10:41:24",
"is_owner": true,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
}
}
]
}
```
**에러 응답**
@@ -707,7 +896,8 @@ Authorization: Bearer {YOUR_TOKEN}
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지 — `keyword`/`uuid` 를 모두 생략한 경우 포함) |
| 500 | Internal Server Error | 검색 중 예외 발생 (`user.search_failed`) |
<!-- @generated:end -->
@@ -809,7 +999,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
@@ -1151,11 +1341,85 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (수정된 사용자 — `UserResource::toArray()`, 관계 미로드이므로 modules/plugins/menus/roles/permissions/consents 키는 응답에 포함되지 않음)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| uuid | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) |
| name | string | `API 문서 샘플 사용자` | 사용자 이름 |
| nickname | string\|null | `null` | 닉네임 |
| email | string | `apidoc-sample-user@example.com` | 이메일 주소 |
| avatar | string\|null | `null` | 프로필 아바타 이미지 URL (미설정 시 null) |
| language | string | `ko` | 사용자 언어 설정 (ko: 한국어, en: English) |
| language_label | string | `한국어` | 언어 코드의 현지화 라벨 (`user.language.{code}` 번역) |
| country | string\|null | `KR` | 국가 코드 (ISO 3166-1 alpha-2) |
| status | string | `active` | 계정 상태 (UserStatus Enum: active/inactive/blocked/withdrawn) |
| status_label | string | `활성` | 상태의 사람이 읽는 라벨 (UserStatus::label()) |
| status_variant | string | `success` | 상태 표시 색상/스타일 변형 키 (UserStatus::variant() — UI 배지용) |
| is_admin | boolean | `true` | 관리자 역할 보유 여부 (User::isAdmin()) |
| homepage | string\|null | `https://example.com` | 홈페이지 URL |
| mobile | string\|null | `010-1234-5678` | 휴대폰 번호 |
| phone | string\|null | `null` | 전화번호 |
| zipcode | string\|null | `null` | 우편번호 |
| address | string\|null | `null` | 기본 주소 |
| address_detail | string\|null | `null` | 상세 주소 |
| signature | string\|null | `null` | 서명 |
| bio | string\|null | `null` | 자기소개 |
| last_login_at | string\|null | `2026-07-07 10:41:24` | 마지막 로그인 일시 (미로그인 시 null) |
| email_verified_at | string\|null | `2026-07-08 10:41:24` | 이메일 인증 일시 (미인증 시 null) |
| timezone | string\|null | `Asia/Seoul` | 사용자 시간대 |
| created_at | string | `2026-07-08 10:41:24` | 생성 일시 |
| updated_at | string | `2026-07-08 11:02:07` | 최종 수정 일시 |
| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_read":true,"can_create":true,"can_update":true,"can_delete":true,"can_assign_roles":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (슈퍼관리자 대상은 can_delete=false 강제) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "사용자 정보가 성공적으로 업데이트되었습니다.",
"data": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "예시 이름",
"nickname": null,
"email": "user@example.com",
"avatar": null,
"language": "ko",
"language_label": "한국어",
"country": "KR",
"status": "active",
"status_label": "활성",
"status_variant": "success",
"is_admin": false,
"homepage": "https://example.com",
"mobile": "010-1234-5678",
"phone": null,
"zipcode": null,
"address": null,
"address_detail": null,
"signature": null,
"bio": null,
"last_login_at": null,
"email_verified_at": null,
"timezone": "Asia/Seoul",
"created_at": "2026-07-08 10:41:24",
"updated_at": "2026-07-08 11:02:07",
"is_owner": false,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
}
}
}
```
**에러 응답**
@@ -1164,7 +1428,8 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지 — 마지막 관리자 본인의 admin 역할 제거 시도 시 `user.last_admin_role_cannot_remove` 포함) |
| 500 | Internal Server Error | 수정 중 예외 발생 (`user.update_failed`, `errors.error` 에 예외 메시지) |
<!-- @generated:end -->
+10 -3
View File
@@ -45,18 +45,25 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-401 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — 컨트롤러가 `ResponseHelper::success('user.password_verified')` 를 인자 없이 호출하므로 `data` 는 `null` 입니다)._
**응답 예시**
<!-- 실측 제외: http-401 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "비밀번호가 확인되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 401 | 비밀번호 불일치 | 전달한 `password` 가 저장된 해시와 일치하지 않는 경우 (`message`: 비밀번호가 일치하지 않습니다.) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | 비밀번호 확인 실패 | 확인 처리 중 예기치 못한 예외가 발생한 경우 (`message`: 비밀번호 확인에 실패했습니다.) |
<!-- @generated:end -->
@@ -11,6 +11,8 @@
- 예약 작업에 등록할 수 없는 Artisan 명령·옵션을 안내하는 문구 일본어 번역 추가 — 왜 등록이 거부됐는지(허용 목록 밖 / 형식 오류 / 허용되지 않은 옵션 / 추가 인자 불가)가 사유별로 표시됩니다.
- 언어팩 설치 시 거부 사유를 안내하는 문구 일본어 번역 추가 — 허용되지 않는 파일 형식, 번역 배열이 아닌 PHP 파일(줄 번호 포함), 심볼릭 링크, 그리고 설치 직후 활성화에 필요한 권한 안내입니다.
- 환경설정 고급 탭의 목록 상한 항목명(총 건수 집계 상한·페이지 번호 상한) 일본어 번역 추가 — 값이 범위를 벗어났을 때의 안내에 내부 식별자 대신 항목명이 표시됩니다.
- 공통 응답 메시지 일본어 번역 추가 (`common.created`·`common.updated`·`common.deleted`) — 생성·수정·삭제 성공 응답이 일본어 로케일에서 번역문으로 표시됩니다.
- 본인인증 정책 삭제 제한 안내 메시지 일본어 번역 추가 (`identity.errors.cannot_delete_system_policy`) — 시스템이 선언한 정책은 삭제할 수 없다는 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
### Changed
@@ -30,4 +30,7 @@ return [
'version_format' => ':attributeのフォーマットが正しくありません。(例:1.0.0、1.0.0-beta.1)',
'to_version_required' => '開始バージョンが指定されている場合、終了バージョンも必須です。',
],
'created' => '作成されました。',
'updated' => '編集されました。',
'deleted' => '削除されました。',
];
@@ -29,6 +29,7 @@ return [
'missing_scope_or_target' => 'ポリシー照会には scope と target の両方が必要です。',
'admin_policy_has_no_default' => '管理者が直接作成したポリシーには宣言デフォルト値がありません。',
'reset_field_failed' => '宣言デフォルト値の復元に失敗しました。フィールドが有効であることを確認してください。',
'cannot_delete_system_policy' => 'システムが宣言したポリシーは削除できません。管理者が直接作成したポリシーのみ削除できます。',
],
'messages' => [
'challenge_requested' => '本人認証コードを送信しました。',
@@ -14,6 +14,7 @@
- 주문 상세의 현금영수증 카드 문구(발급 상태 안내, 발급·발급취소·재발급 버튼, 발급 이력 표) 일본어 번역 추가 — 구매자와 관리자가 보는 현금영수증 영역이 일본어 로케일에서 자연스럽게 표시됩니다.
- 환불 계좌 입력 안내와 검증 문구(`validation.order.refund_bank_*`) 일본어 번역 추가 — 주문서와 관리자 취소 화면의 환불 계좌 입력란이 일본어 로케일에서 자연스럽게 표시됩니다.
- 현금영수증 자진발급 설정의 국세청 의무발행 업종 안내 링크 문구(`order_settings.cash_receipt.self_issue_mandatory_link`) 일본어 번역 추가 — 자진발급을 켤지 판단할 때 참고하는 국세청 안내 링크가 일본어 로케일에서 자연스럽게 표시됩니다.
- 카테고리 이미지 처리 결과 안내 문구(`category_images.*` — 업로드·삭제·순서 변경·이미지 없음) 일본어 번역 추가 — 관리자가 카테고리 이미지를 등록·삭제·정렬할 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
## [1.0.8] - 2026-08-01
@@ -15,12 +15,6 @@ return [
'status_changed' => 'カテゴリの状態が変更されました。',
'order_updated' => 'カテゴリの順序が変更されました。',
],
'category_images' => [
'uploaded' => 'カテゴリ画像がアップロードされました。',
'not_found' => 'カテゴリ画像が見つかりません。',
'deleted' => 'カテゴリ画像が削除されました。',
'reordered' => 'カテゴリ画像の順序が変更されました。',
],
'settings' => [
'fetch_success' => '設定を閲覧しました。',
'fetch_failed' => '設定の取得に失敗しました。',
@@ -79,6 +73,12 @@ return [
'thumbnail_set' => 'メイン画像が設定されました。',
'not_found' => '商品画像が見つかりません。',
],
'category_images' => [
'uploaded' => '画像がアップロードされました。',
'deleted' => '画像が削除されました。',
'reordered' => '画像の順序が変更されました。',
'not_found' => 'カテゴリー画像が見つかりません。',
],
'brands' => [
'list_retrieved' => 'ブランド一覧を閲覧しました。',
'retrieved' => 'ブランド情報を閲覧しました。',
+3
View File
@@ -9,6 +9,9 @@ return [
'unauthorized' => 'Authentication is required.',
'forbidden' => 'Access denied.',
'validation_failed' => 'Input validation failed.',
'created' => 'Created successfully.',
'updated' => 'Updated successfully.',
'deleted' => 'Deleted successfully.',
// Button states
'saving' => 'Saving...',
+1
View File
@@ -30,6 +30,7 @@ return [
'missing_scope_or_target' => 'Both scope and target are required to resolve a policy.',
'admin_policy_has_no_default' => 'Admin-created policies do not have a declared default.',
'reset_field_failed' => 'Failed to reset the field to its declared default. Check if the field is valid.',
'cannot_delete_system_policy' => 'System-declared policies cannot be deleted. Only administrator-created policies can be deleted.',
],
'messages' => [
+3
View File
@@ -9,6 +9,9 @@ return [
'unauthorized' => '인증이 필요합니다.',
'forbidden' => '권한이 없습니다.',
'validation_failed' => '입력 값 검증에 실패했습니다.',
'created' => '생성되었습니다.',
'updated' => '수정되었습니다.',
'deleted' => '삭제되었습니다.',
// 버튼 상태
'saving' => '저장 중...',
+1
View File
@@ -30,6 +30,7 @@ return [
'missing_scope_or_target' => '정책 조회를 위해 scope 와 target 이 모두 필요합니다.',
'admin_policy_has_no_default' => '관리자가 직접 생성한 정책에는 선언 기본값이 없습니다.',
'reset_field_failed' => '선언 기본값 복원에 실패했습니다. 필드가 유효한지 확인하세요.',
'cannot_delete_system_policy' => '시스템이 선언한 정책은 삭제할 수 없습니다. 관리자가 직접 생성한 정책만 삭제할 수 있습니다.',
],
'messages' => [
@@ -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 -->

Some files were not shown because too many files have changed in this diff Show More