From 56a78e1d55d6bacc2a02d75ce907de99400c2612 Mon Sep 17 00:00:00 2001 From: HeuJung Date: Wed, 8 Jul 2026 17:48:53 +0900 Subject: [PATCH] =?UTF-8?q?feat(api-docs):=20=EC=9A=94=EC=B2=AD/=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=98=88=EC=8B=9C=20=EB=B8=94=EB=A1=9D=20+=208?= =?UTF-8?q?=EC=A2=85=20=EA=B2=B0=ED=95=A8=20=EA=B7=BC=EB=B3=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20+=20audit=20error=20=EC=8A=B9=EA=B2=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API 레퍼런스에 요청 예시(raw HTTP)·응답 예시(envelope 전문)를 표준 포맷에 편입하고, 전 대상(코어+전 모듈+전 플러그인)에 방출했다. 예시 결함 8종 (multipart·Host 마스킹·write 실측·path 치환·현실적 바디·query 반영·중복 제거·민감값 마스킹)을 스캐폴더/프로브 공용 수정으로 근본 해소했다. 추가로 검증에서 드러난 결함을 마감했다: - 응답 예시 body 의 절대 URL(페이지네이터·콜백) 호스트를 placeholder 로 마스킹(maskResponseHost) — 요청 Host 헤더만 마스킹되던 누락 보완, 확장 39건 정정 - api:docgen 커맨드 통합 테스트 신설(라우트 수집·확장 라우팅·rules 추출· 멱등 재생성·check drift) - module-routing.md URL 스킴 오류 정정(/api/admin → /api/modules) - README·cheatsheet·api-documentation 에 API 문서 커맨드/레퍼런스 진입점 반영 전 대상 3칸 완료로 api-doc-coverage 룰을 warn→error 승격. --- AGENTS.md | 4 +- README.md | 2 +- app/Console/Commands/ApiDocgenCommand.php | 306 +- app/Support/ApiDoc/ApiDocScaffolder.php | 749 +- app/Support/ApiDoc/ApiEndpointProbe.php | 87 +- app/Support/ApiDoc/ApiRouteInventory.php | 24 +- app/Support/ApiDoc/ResourceFieldDescriber.php | 39 + docs/backend/README.md | 2 +- docs/backend/api-documentation.md | 111 +- docs/backend/api/activity-logs.md | 150 +- docs/backend/api/attachment.md | 12 + docs/backend/api/attachments.md | 99 + docs/backend/api/auth.md | 2432 +++- docs/backend/api/avatar.md | 34 + docs/backend/api/broadcasting.md | 13 + docs/backend/api/changelog.md | 25 + docs/backend/api/core-update.md | 1548 +++ docs/backend/api/dashboard.md | 217 + docs/backend/api/extensions.md | 56 + docs/backend/api/identity.md | 1219 +- docs/backend/api/language-packs.md | 483 +- docs/backend/api/layouts.md | 26 + docs/backend/api/license.md | 25 + docs/backend/api/locales.md | 33 + docs/backend/api/me.md | 110 + docs/backend/api/menus.md | 881 +- docs/backend/api/modules.md | 11431 +++++++++++++++- docs/backend/api/notification-channels.md | 58 + docs/backend/api/notification-definitions.md | 499 +- docs/backend/api/notification-logs.md | 118 +- docs/backend/api/notification-templates.md | 134 +- docs/backend/api/notifications.md | 334 +- docs/backend/api/password.md | 20 + docs/backend/api/permissions.md | 4541 ++++++ docs/backend/api/plugins.md | 532 +- docs/backend/api/profile.md | 76 + docs/backend/api/roles.md | 9180 ++++++++++++- docs/backend/api/schedules.md | 603 +- docs/backend/api/search.md | 25 + docs/backend/api/seo.md | 126 +- docs/backend/api/settings.md | 745 +- docs/backend/api/templates.md | 997 +- docs/backend/api/users.md | 640 +- docs/backend/api/verify-password.md | 18 + docs/cheatsheet.md | 16 + docs/extension/module-routing.md | 4 +- .../gnuboard7-hello_module/docs/api/memos.md | 18 + .../sirsoft-board/docs/api/activity-stats.md | 27 + .../docs/api/board-activities.md | 78 + .../sirsoft-board/docs/api/board-types.md | 98 + .../_bundled/sirsoft-board/docs/api/board.md | 1087 +- .../_bundled/sirsoft-board/docs/api/boards.md | 2274 ++- .../sirsoft-board/docs/api/dashboard.md | 167 + .../sirsoft-board/docs/api/my-comments.md | 72 + .../sirsoft-board/docs/api/reports.md | 290 +- .../sirsoft-board/docs/api/settings.md | 643 +- .../_bundled/sirsoft-board/docs/api/users.md | 98 + .../sirsoft-ecommerce/docs/api/addresses.md | 340 +- .../sirsoft-ecommerce/docs/api/brands.md | 261 +- .../sirsoft-ecommerce/docs/api/cart.md | 373 +- .../sirsoft-ecommerce/docs/api/categories.md | 586 +- .../docs/api/category-image.md | 12 + .../sirsoft-ecommerce/docs/api/checkout.md | 92 + .../docs/api/claim-reasons.md | 453 +- .../sirsoft-ecommerce/docs/api/coupons.md | 180 + .../sirsoft-ecommerce/docs/api/currency.md | 43 + .../sirsoft-ecommerce/docs/api/dashboard.md | 166 + .../docs/api/extra-fee-templates.md | 333 +- .../sirsoft-ecommerce/docs/api/guest.md | 102 + .../sirsoft-ecommerce/docs/api/inquiries.md | 217 + .../docs/api/mileage-transactions.md | 184 +- .../sirsoft-ecommerce/docs/api/mileage.md | 132 + .../sirsoft-ecommerce/docs/api/options.md | 77 + .../sirsoft-ecommerce/docs/api/orders.md | 1412 +- .../sirsoft-ecommerce/docs/api/payments.md | 12 + .../sirsoft-ecommerce/docs/api/presets.md | 80 + .../docs/api/product-common-infos.md | 283 +- .../docs/api/product-image.md | 12 + .../docs/api/product-labels.md | 236 +- .../docs/api/product-notice-templates.md | 344 +- .../sirsoft-ecommerce/docs/api/products.md | 2914 +++- .../docs/api/promotion-coupons.md | 530 +- .../docs/api/review-image.md | 12 + .../sirsoft-ecommerce/docs/api/reviews.md | 540 +- .../sirsoft-ecommerce/docs/api/settings.md | 3254 ++++- .../docs/api/shipping-carriers.md | 396 +- .../docs/api/shipping-country.md | 61 +- .../docs/api/shipping-policies.md | 416 +- .../sirsoft-ecommerce/docs/api/users.md | 54 +- .../sirsoft-ecommerce/docs/api/wishlist.md | 83 +- .../sirsoft-page/docs/api/attachments.md | 66 + .../_bundled/sirsoft-page/docs/api/pages.md | 517 +- .../sirsoft-ckeditor5/docs/api/images.md | 8 + .../sirsoft-ckeditor5/docs/api/upload.md | 17 + .../Support/ApiDoc/ApiDocSampleService.php | 80 + .../Unit/Support/ApiDocSampleServiceTest.php | 83 + .../sirsoft-gdpr/docs/api/consent-log.md | 33 + .../_bundled/sirsoft-gdpr/docs/api/consent.md | 221 +- .../sirsoft-gdpr/docs/api/policy-versions.md | 154 +- .../sirsoft-gdpr/docs/api/settings.md | 265 + .../Support/ApiDoc/ApiDocSampleService.php | 79 + .../Unit/Support/ApiDocSampleServiceTest.php | 83 + .../sirsoft-marketing/docs/api/channels.md | 9 + .../sirsoft-marketing/docs/api/settings.md | 8 + .../sirsoft-pay_kginicis/docs/api/cbt.md | 82 +- .../sirsoft-pay_kginicis/docs/api/orders.md | 469 +- .../sirsoft-pay_kginicis/docs/api/payment.md | 88 + .../docs/api/transaction.md | 13 + .../sirsoft-pay_kginicis/docs/api/vbank.md | 29 +- .../docs/api/identity.md | 15 +- .../Feature/Console/ApiDocgenCommandTest.php | 164 + .../Console/ApiEndpointProbeWriteTest.php | 110 + .../Support/ApiDoc/ApiDocPipelineTest.php | 531 + .../ApiDoc/ResourceFieldDescriberTest.php | 29 +- 114 files changed, 60782 insertions(+), 197 deletions(-) create mode 100644 plugins/_bundled/sirsoft-ckeditor5/src/Support/ApiDoc/ApiDocSampleService.php create mode 100644 plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Support/ApiDocSampleServiceTest.php create mode 100644 plugins/_bundled/sirsoft-gdpr/src/Support/ApiDoc/ApiDocSampleService.php create mode 100644 plugins/_bundled/sirsoft-gdpr/tests/Unit/Support/ApiDocSampleServiceTest.php create mode 100644 tests/Feature/Console/ApiDocgenCommandTest.php create mode 100644 tests/Feature/Console/ApiEndpointProbeWriteTest.php diff --git a/AGENTS.md b/AGENTS.md index b7f70b5c..fa6b75dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ | [activity-log-hooks.md](docs/backend/activity-log-hooks.md) | 활동 로그 훅 레퍼런스 (Activity Log Hooks Reference) | 코어 66훅 + 이커머스 92훅 + 게시판 32훅 + 페이지 8훅 = 총 198훅 | | [activity-log.md](docs/backend/activity-log.md) | 활동 로그 시스템 (Activity Log System) | Monolog 기반: Service 훅 → Listener → Log::channel('activity... | | [admin-settings-access.md](docs/backend/admin-settings-access.md) | Admin 환경설정 값 접근 (`g7_core_settings` vs `config()`) | 동기화 SSoT: storage/app/settings/*.json → SettingsServicePr... | -| [api-documentation.md](docs/backend/api-documentation.md) | API 레퍼런스 문서 규정 (API Documentation) | 모든 API 엔드포인트는 레퍼런스 문서 필수 — 메서드/URI/파라미터/응답 필드 전수 기재 | +| [api-documentation.md](docs/backend/api-documentation.md) | API 레퍼런스 문서 규정 (API Documentation) | 모든 API 엔드포인트는 레퍼런스 문서 필수 — 메서드/URI/파라미터/응답 필드 + 요청·응답 예시 ... | | [api-resources.md](docs/backend/api-resources.md) | API 리소스 | Resource: BaseApiResource 상속 필수 / Collection: BaseApiColl... | | [authentication.md](docs/backend/authentication.md) | 인증 및 세션 처리 | Laravel Sanctum 토큰 전용 인증 (Bearer 토큰만 사용) | | [broadcasting.md](docs/backend/broadcasting.md) | Broadcasting (실시간 이벤트) | Laravel Reverb 사용 (WebSocket) | @@ -117,7 +117,7 @@ | [module-i18n.md](docs/extension/module-i18n.md) | 모듈 다국어 시스템 | 백엔드: /lang/{locale}/*.php → __('vendor-module::key') | | [module-identity-settings.md](docs/extension/module-identity-settings.md) | 모듈/플러그인 본인인증(IDV) 설정 통합 가이드 | 정책/목적/메시지: module.php::getIdentity{Policies,Purposes,Mess... | | [module-layouts.md](docs/extension/module-layouts.md) | 모듈 레이아웃 시스템 | 위치: modules/_bundled/vendor-module/resources/layouts/admi... | -| [module-routing.md](docs/extension/module-routing.md) | 모듈 라우트 규칙 | URL prefix 자동: /api/admin/[vendor-module]/... | +| [module-routing.md](docs/extension/module-routing.md) | 모듈 라우트 규칙 | URL prefix 자동: /api/modules/[vendor-module]/... | | [module-settings.md](docs/extension/module-settings.md) | 모듈 환경설정 시스템 개발 가이드 | - | | [permissions.md](docs/extension/permissions.md) | 권한 시스템 | 구조: User → Role → Permission (기능 레벨) | | [plugin-development.md](docs/extension/plugin-development.md) | 플러그인 개발 가이드 | 디렉토리: plugins/vendor-plugin (예: sirsoft-payment) | diff --git a/README.md b/README.md index 8514de61..a5929011 100644 --- a/README.md +++ b/README.md @@ -462,7 +462,7 @@ cp .env.example .env | 플러그인 개발 | [docs/extension/plugin-development.md](docs/extension/plugin-development.md) | | 템플릿 개발 | [docs/extension/template-basics.md](docs/extension/template-basics.md) | | 테스트 | [docs/testing-guide.md](docs/testing-guide.md) | -| API 레퍼런스 | 준비 중 | +| API 레퍼런스 | [docs/backend/api-documentation.md](docs/backend/api-documentation.md) | --- diff --git a/app/Console/Commands/ApiDocgenCommand.php b/app/Console/Commands/ApiDocgenCommand.php index aae951fc..3e68c1ff 100644 --- a/app/Console/Commands/ApiDocgenCommand.php +++ b/app/Console/Commands/ApiDocgenCommand.php @@ -38,6 +38,7 @@ class ApiDocgenCommand extends Command {--user= : 실측 토큰 발급 대상 사용자 ID} {--seed : 실측 전 완전 샘플 데이터를 시드 (개발 환경 전용)} {--check : 생성하지 않고 누락/미실측만 리포트} + {--examples-only : 기존 문서에 요청/응답 예시 블록만 in-place 삽입 (표·서술 불가침, 재생성 금지)} {--dry-run : 생성 대상만 출력}'; /** @@ -123,16 +124,18 @@ class ApiDocgenCommand extends Command $this->info('실측 기준 URL: '.$probe->baseUrl()); } - $stats = ['files' => 0, 'endpoints' => 0, 'probed' => 0, 'skipped' => 0]; + $stats = ['files' => 0, 'endpoints' => 0, 'probed' => 0, 'skipped' => 0, 'examples' => 0]; $checkFindings = []; + $examplesOnly = (bool) $this->option('examples-only'); foreach ($grouped as $file => $items) { $sections = []; $sectionKeys = []; + $exampleBlocks = []; foreach ($items as $route) { $request = $introspector->introspect($route['controller'], $route['controller_method']); - [$schema, $probeMeta] = $this->probeEndpoint($probe, $inferrer, $route, $probed, $sampleMap); + [$schema, $probeMeta] = $this->probeEndpoint($probe, $inferrer, $route, $probed, $sampleMap, $request); if ($probeMeta['skipped_reason'] === null) { $stats['probed']++; @@ -141,9 +144,28 @@ class ApiDocgenCommand extends Command $checkFindings[] = "{$route['method']} {$route['uri']} — {$probeMeta['skipped_reason']}"; } + $key = $route['name'] ?: $route['uri']; + + if ($examplesOnly) { + // 표·서술을 건드리지 않고 예시 2블록만 산출 (SSoT=스캐폴더 예시 메서드). + // 단, 응답 필드 표가 아직 실측 제외 마커(사람이 채운 셀 없음)인데 이번 실측으로 + // 스키마가 확보됐으면 그 마커를 실측 필드 표로 갱신한다(쓰기 메서드 실측 도입분). + $commentMap = $this->columnComments($route, $commentResolver, $sampleMap); + $exampleBlocks[$key] = [ + 'request' => $scaffolder->requestExampleBlock($route, $request, $probeMeta), + 'response' => $scaffolder->responseExampleBlock($schema, $probeMeta), + 'response_fields' => $schema !== null + ? $scaffolder->responseFieldTableBlock($schema, $probeMeta, $commentMap) + : null, + ]; + $stats['endpoints']++; + + continue; + } + $commentMap = $this->columnComments($route, $commentResolver, $sampleMap); $sections[] = $scaffolder->endpointSection($route, $request, $schema, $probeMeta, $commentMap); - $sectionKeys[] = $route['name'] ?: $route['uri']; + $sectionKeys[] = $key; $stats['endpoints']++; } @@ -155,6 +177,28 @@ class ApiDocgenCommand extends Command continue; } + // examples-only: 재생성 없이 기존 문서에 예시 블록만 in-place 삽입. + // 전체 재생성은 @generated 블록 내부 표(파라미터/응답 필드)의 사람 서술을 + // TODO 로 되돌리므로, 예시 방출을 위해 재생성을 쓰지 않는다. + if ($examplesOnly) { + if (! File::exists($file)) { + $this->warn("문서 없음(예시 삽입 건너뜀): {$file}"); + + continue; + } + + $existing = File::get($file); + [$updated, $inserted] = $scaffolder->insertExampleBlocks($existing, $exampleBlocks); + + if ($updated !== $existing) { + File::put($file, $updated); + $stats['files']++; + } + $stats['examples'] += $inserted; + + continue; + } + $header = $this->documentHeader($file, $items[0]); $existing = File::exists($file) ? File::get($file) : null; $content = $scaffolder->mergeDocument($existing, $header, $sections, $sectionKeys); @@ -182,6 +226,18 @@ class ApiDocgenCommand extends Command } $this->newLine(); + if ($examplesOnly) { + $this->info(sprintf( + '예시 삽입 완료: 파일 %d개, 예시 블록 %d개 (실측 %d, 제외 %d)', + $stats['files'], + $stats['examples'], + $stats['probed'], + $stats['skipped'] + )); + + return self::SUCCESS; + } + $this->info(sprintf( '완료: 파일 %d개, 엔드포인트 %d개 (실측 %d, 제외 %d)', $stats['files'], @@ -277,22 +333,181 @@ class ApiDocgenCommand extends Command * @param array $route 라우트 메타데이터 * @param bool $probed 실측 가능 여부 * @param array $sampleMap 도메인별 대표 샘플 맵 + * @param array $request FormRequest 분석 결과 (쓰기 바디 구성용) * @return array{0: array|null, 1: array} 스키마와 실측 메타 */ - private function probeEndpoint(ApiEndpointProbe $probe, ResponseSchemaInferrer $inferrer, array $route, bool $probed, array $sampleMap = []): array + private function probeEndpoint(ApiEndpointProbe $probe, ResponseSchemaInferrer $inferrer, array $route, bool $probed, array $sampleMap = [], array $request = []): array { + $baseUrl = $probe->baseUrl(); + if (! $probed) { - return [null, ['status' => null, 'skipped_reason' => 'no-token']]; + return [null, ['status' => null, 'skipped_reason' => 'no-token', 'base_url' => $baseUrl, 'resolved_uri' => null, 'body' => null]]; } $uri = $this->resolvePathParams($route, $sampleMap); - $result = $probe->probe($route['method'], $uri); + $isWrite = in_array($route['method'], ['POST', 'PUT', 'PATCH', 'DELETE'], true); - if (! $result['ok'] || $result['body'] === null) { - return [null, ['status' => $result['status'], 'skipped_reason' => $result['skipped_reason'] ?? ('http-'.$result['status'])]]; + // 요청 예시에 노출할 정규 URI (실측용 per_page 보정 제외). + $exampleUri = $this->exampleUri($route, $sampleMap); + + // 부수효과가 트랜잭션 롤백으로 되돌릴 수 없는 쓰기(확장 install/activate/update, + // 언어팩 설치, 코어 업데이트, 파일 업로드 등)는 in-process 실측에서 제외한다. + // 이들은 파일시스템·프로세스·외부 네트워크를 건드려 롤백 불가·hang 위험이 있다. + if ($isWrite && $this->isSideEffectfulWrite($route)) { + return [null, [ + 'status' => null, + 'skipped_reason' => 'side-effectful-write', + 'base_url' => $baseUrl, + 'resolved_uri' => $exampleUri, + 'body' => null, + ]]; } - return [$inferrer->infer($result['body']), ['status' => $result['status'], 'skipped_reason' => null]]; + // 쓰기 메서드는 FormRequest 규칙 기반 실측 바디를 만들어 in-process 롤백 실측에 사용한다. + $writeBody = in_array($route['method'], ['POST', 'PUT', 'PATCH'], true) + ? $this->buildWriteBody($request, $sampleMap) + : []; + $result = $probe->probe($route['method'], $uri, $writeBody); + + if (! $result['ok'] || $result['body'] === null) { + return [null, [ + 'status' => $result['status'], + 'skipped_reason' => $result['skipped_reason'] ?? ('http-'.$result['status']), + 'base_url' => $baseUrl, + 'resolved_uri' => $exampleUri, + 'body' => null, + ]]; + } + + return [$inferrer->infer($result['body']), [ + 'status' => $result['status'], + 'skipped_reason' => null, + 'base_url' => $baseUrl, + 'resolved_uri' => $exampleUri, + 'body' => $result['body'], + ]]; + } + + /** + * 부수효과가 트랜잭션 롤백으로 되돌릴 수 없는 쓰기 엔드포인트인지 판별합니다. + * + * 확장(모듈/플러그인/템플릿) 및 언어팩 설치·활성·업데이트, 코어 업데이트, 파일 업로드, + * 캐시/워밍업/생성 등 파일시스템·프로세스·외부 네트워크를 건드리는 쓰기는 in-process 롤백 + * 실측이 불가능(hang·비가역 부수효과)하므로 실측에서 제외한다. URI 세그먼트로 판정한다. + * + * @param array $route 라우트 메타데이터 + * @return bool 부수효과 쓰기 여부 + */ + private function isSideEffectfulWrite(array $route): bool + { + $uri = (string) $route['uri']; + + // 확장/언어팩 관리 도메인의 쓰기는 전량 제외 (install/activate/... 은 실환경 변경). + if (preg_match('#/api/admin/(modules|plugins|templates|language-packs|core-update)(/|$)#', $uri)) { + return true; + } + + // 동작성 세그먼트를 가진 쓰기 (도메인 무관). + $unsafeSegments = [ + 'install', 'install-from-file', 'install-from-github', 'install-from-url', 'install-from-bundled', + 'uninstall', 'activate', 'deactivate', 'bulk-activate', 'update', 'check-updates', + 'refresh-cache', 'clear', 'warmup', 'generate', 'sync', 'backup', 'restore', + 'upload', 'import', 'export', 'manifest-preview', 'preview', + ]; + + foreach ($unsafeSegments as $seg) { + if (str_contains($uri, '/'.$seg)) { + return true; + } + } + + return false; + } + + /** + * 요청 예시에 노출할 정규 URI 를 산출합니다. + * + * `resolvePathParams` 는 목록 GET 실측용으로 `?per_page=25` 를 덧붙이지만, + * 요청 예시에는 이 실측 보정을 제외한 순수 경로(치환된 path 파라미터 포함)를 노출한다. + * path 파라미터가 치환되지 않은(실측 제외) 경우 `{param}` placeholder 를 그대로 남긴다. + * + * @param array $route 라우트 메타데이터 + * @param array $sampleMap 도메인별 대표 샘플 맵 + * @return string 요청 예시용 URI + */ + private function exampleUri(array $route, array $sampleMap): string + { + $resolved = $this->resolvePathParams($route, $sampleMap); + + // 실측용 per_page 보정 쿼리를 제거해 정규 경로만 남긴다. + return preg_replace('/[?&]per_page=25$/', '', $resolved) ?? $resolved; + } + + /** + * 쓰기 메서드 in-process 실측용 요청 바디를 FormRequest 규칙 근거로 구성합니다. + * + * 필수 파라미터를 우선하고, 검증을 통과할 수 있도록 이름·타입·허용값에 맞는 유효값을 채웁니다. + * 파일(image/file) 파라미터는 실측 바디에서 제외한다(in-process dispatch 로 파일 전송 불가 → + * 해당 엔드포인트는 http-422 로 스킵되고 정적 요청 예시[multipart]만 방출된다). + * + * @param array $request FormRequest 분석 결과 + * @param array $sampleMap 도메인별 대표 샘플 맵 + * @return array 실측 요청 바디 + */ + private function buildWriteBody(array $request, array $sampleMap = []): array + { + $params = $request['params'] ?? []; + $body = []; + + foreach ($params as $p) { + $name = (string) $p['name']; + $type = (string) ($p['type'] ?? 'string'); + + // 파일 타입은 in-process 바디로 전송 불가 → 생략. + if (in_array($type, ['file', 'image'], true)) { + continue; + } + + $body[$name] = $this->writeValue($name, $type, (string) ($p['allowed'] ?? ''), $sampleMap); + } + + return $body; + } + + /** + * 쓰기 실측 바디의 파라미터 값을 검증 통과 가능하도록 생성합니다. + * + * @param string $name 파라미터명 + * @param string $type 타입 + * @param string $allowed 허용값 설명 (in: 열거 등) + * @param array $sampleMap 도메인 샘플 맵 + * @return mixed 유효 예시값 + */ + private function writeValue(string $name, string $type, string $allowed, array $sampleMap): mixed + { + $lower = strtolower($name); + + // 허용값 in: 열거(백틱)가 있으면 첫 값을 채택(검증 통과 보장). + if (preg_match('/`([^`]+)`/', $allowed, $m)) { + return $m[1]; + } + + return match (true) { + $type === 'boolean' => true, + $type === 'integer', $type === 'number' => 1, + $type === 'array' => [], + $type === 'email' || str_contains($lower, 'email') => 'probe_'.uniqid().'@example.com', + $type === 'uuid' => (string) Str::uuid(), + $type === 'date' => '2026-01-01', + str_contains($lower, 'password') => 'Password123!', + str_contains($lower, 'url') || str_contains($lower, 'homepage') => 'https://example.com', + // identifier/slug 는 소문자/숫자/밑줄만 허용하는 규칙이 흔하므로 하이픈 없이 생성. + str_contains($lower, 'identifier') || str_contains($lower, 'slug') => 'probe_'.uniqid(), + str_contains($lower, 'locale') || $lower === 'language' => 'ko', + str_contains($lower, 'country') => 'KR', + str_contains($lower, 'timezone') => 'Asia/Seoul', + default => '실측 예시값', + }; } /** @@ -329,6 +544,17 @@ class ApiDocgenCommand extends Command if ($explicit !== null) { $uri = str_replace('{'.$param.'}', (string) $explicit, $uri); + + continue; + } + + // 코어 다수 라우트는 route-model binding 없이 `{id}`/`{identifier}`/`{key}` 문자열 + // 파라미터를 쓰고 컨트롤러가 직접 조회한다. 도메인 대표 샘플 모델의 route key(또는 + // 해당 컬럼)로 치환해 실측 가능하게 한다. (마지막 세그먼트 리소스 파라미터에 한함) + $generic = $this->resolveGenericPathParam($param, $domain, $sampleMap); + + if ($generic !== null) { + $uri = str_replace('{'.$param.'}', (string) $generic, $uri); } continue; @@ -351,6 +577,64 @@ class ApiDocgenCommand extends Command return $uri; } + /** + * route-model binding 없는 제네릭 path 파라미터를 도메인 샘플 모델의 실제 키로 치환합니다. + * + * 코어 다수 라우트는 `{id}`/`{identifier}`/`{key}`/`{slug}` 문자열 파라미터를 쓰고 컨트롤러가 + * 직접 조회한다. 도메인 대표 샘플 모델을 찾아 파라미터명에 대응하는 컬럼값으로 치환한다: + * - `id` → 모델 기본키 + * - `identifier`/`slug`/`key`/`code` → 같은 이름 컬럼이 있으면 그 값, 없으면 route key + * - 그 외 → 치환 안 함(null 반환 → 실측 제외 유지, 오치환 방지) + * + * @param string $param path 파라미터명 + * @param string|null $domain 도메인 그룹명 + * @param array $sampleMap 도메인 샘플 맵 + * @return string|null 치환값 (없으면 null) + */ + private function resolveGenericPathParam(string $param, ?string $domain, array $sampleMap): ?string + { + $modelClass = ($domain !== null && isset($sampleMap[$domain])) + ? $sampleMap[$domain]['model'] + : $this->domainModelHint($domain); + + if (! $modelClass) { + return null; + } + + try { + /** @var Model $model */ + $model = new $modelClass; + $record = $modelClass::query()->orderBy($model->getKeyName())->first(); + + if (! $record) { + return null; + } + + $lower = strtolower($param); + + // id 계열 → 기본키 + if ($lower === 'id' || str_ends_with($lower, '_id')) { + return (string) $record->getKey(); + } + + // identifier/slug/key/code → 동일 컬럼이 있으면 그 값 + if (in_array($lower, ['identifier', 'slug', 'key', 'code'], true)) { + $val = $record->getAttribute($lower); + + return $val !== null ? (string) $val : (string) $record->getRouteKey(); + } + + // 도메인 단수 리소스명 파라미터 → route key + if ($domain !== null && $this->paramMatchesDomain($param, $domain)) { + return (string) $record->getRouteKey(); + } + + return null; + } catch (\Throwable) { + return null; + } + } + /** * path 파라미터명이 도메인의 단수 리소스명과 일치하는지 판정합니다. * @@ -530,8 +814,8 @@ class ApiDocgenCommand extends Command ```text 1. 이 문서는 실제 API 호출로 실측한 {$domain} 엔드포인트 레퍼런스입니다 - 2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 - 3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 + 2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope) + 3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다 4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 5. 설명(TODO) 칸은 사람이 채웁니다 ``` diff --git a/app/Support/ApiDoc/ApiDocScaffolder.php b/app/Support/ApiDoc/ApiDocScaffolder.php index f39de45c..fd6863dc 100644 --- a/app/Support/ApiDoc/ApiDocScaffolder.php +++ b/app/Support/ApiDoc/ApiDocScaffolder.php @@ -22,6 +22,28 @@ class ApiDocScaffolder */ private const GEN_END = ''; + /** + * @var int 응답 예시의 목록(`data.data[]`) 최대 표시 항목 수 + * + * 목록 응답 body 는 클 수 있으므로 대표 항목만 남기고 나머지는 절단 주석으로 대체한다. + */ + private const LIST_EXAMPLE_LIMIT = 2; + + /** + * @var string 요청 예시의 토큰 마스킹 placeholder + * + * 실측 토큰은 임시 발급분이므로 문서에 평문 유출하지 않고 placeholder 로 마스킹한다. + */ + private const TOKEN_PLACEHOLDER = '{YOUR_TOKEN}'; + + /** + * @var string 요청 예시의 공개 Host placeholder + * + * 실측 기준 URL(로컬 개발 호스트 등)을 공개 문서에 노출하지 않고 중립 placeholder 로 마스킹한다. + * RFC 2606 예약 도메인(example.com) 기반이라 어느 환경에도 종속되지 않는다. + */ + private const DOC_HOST = 'api.example.com'; + /** * @param ResourceFieldDescriber $fieldDescriber accessor/computed 필드 설명기 * @param ParameterDescriber $paramDescriber 공통 요청 파라미터 설명기 @@ -37,7 +59,7 @@ class ApiDocScaffolder * @param array $route 라우트 메타데이터 * @param array $request FormRequest 분석 결과 * @param array|null $schema 실측 응답 스키마 (null=실측 안 됨) - * @param array $probeMeta 실측 메타 (status, skipped_reason) + * @param array $probeMeta 실측 메타 (status, skipped_reason, base_url, resolved_uri, body) * @param array $commentMap 컬럼명 => 주석 (필드 설명 기본값) * @return string 마크다운 섹션 */ @@ -67,11 +89,21 @@ class ApiDocScaffolder } $lines[] = ''; + $lines[] = '**요청 예시**'; + $lines[] = ''; + $lines[] = $this->requestExampleBlock($route, $request, $probeMeta); + $lines[] = ''; + $lines[] = '**응답 필드** (`data` 내부)'; $lines[] = ''; $lines[] = $this->responseFieldTable($schema, $probeMeta, $commentMap); $lines[] = ''; + $lines[] = '**응답 예시**'; + $lines[] = ''; + $lines[] = $this->responseExampleBlock($schema, $probeMeta); + $lines[] = ''; + $lines[] = '**에러 응답**'; $lines[] = ''; $lines[] = $this->errorTable($route, $request); @@ -141,8 +173,9 @@ class ApiDocScaffolder private function requestParamTable(array $route, array $request): string { $rows = []; + $pathParams = $route['path_params']; - foreach ($route['path_params'] as $pathParam) { + foreach ($pathParams as $pathParam) { $desc = $this->paramDescriber->describe($pathParam, 'path', 'string'); $descCell = $desc !== null ? $this->escapeCell($desc) : ''; $rows[] = "| {$pathParam} | path | string | 예 | — | {$descCell} |"; @@ -151,6 +184,12 @@ class ApiDocScaffolder $location = in_array($route['method'], ['GET', 'DELETE'], true) ? 'query' : 'body'; foreach ($request['params'] as $p) { + // path 파라미터로 이미 출력한 이름은 FormRequest 규칙에서 중복 출력하지 않는다 + // (라우트 바인딩 세그먼트가 FormRequest rules() 에도 있으면 path 행이 우선). + if (in_array($p['name'], $pathParams, true)) { + continue; + } + $required = $p['required'] ? '예' : '아니오'; $allowed = $p['allowed'] !== '' ? $p['allowed'] : '—'; $desc = $this->paramDescriber->describe($p['name'], $location, $p['type']); @@ -165,6 +204,19 @@ class ApiDocScaffolder return "| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |\n| --- | --- | --- | --- | --- | --- |\n".implode("\n", $rows); } + /** + * 응답 필드 표 블록을 생성합니다 (examples-only 모드의 마커 갱신용 공개 래퍼). + * + * @param array|null $schema 실측 응답 스키마 + * @param array $probeMeta 실측 메타 + * @param array $commentMap 컬럼명 => 주석 (필드 설명 기본값) + * @return string 마크다운 표 또는 실측 제외 사유 + */ + public function responseFieldTableBlock(?array $schema, array $probeMeta, array $commentMap = []): string + { + return $this->responseFieldTable($schema, $probeMeta, $commentMap); + } + /** * 응답 필드 표를 생성합니다. * @@ -263,6 +315,437 @@ class ApiDocScaffolder return "| 상태코드 | 의미 | 발생 조건 |\n| --- | --- | --- |\n".implode("\n", $rows); } + /** + * 실제 호출을 재현하는 raw HTTP 요청 예시 블록을 생성합니다. + * + * 요청 라인(`{METHOD} {path} HTTP/1.1`) + 헤더 + 바디로 raw HTTP 요청을 조립합니다 + * (응답 예시의 `HTTP/1.1 {status}` 상태줄과 대칭). curl 은 사용하지 않습니다. + * - 인증 필요(`auth:sanctum`/`optional.sanctum`) 시 `Authorization: Bearer {YOUR_TOKEN}` 마스킹. + * - 바디 있는 메서드(POST/PUT/PATCH)는 요청 파라미터 표의 필수 파라미터 + 타입별 샘플값을 + * 빈 줄 뒤 JSON 바디로 붙입니다. + * + * path 파라미터가 실측 치환값(`resolved_uri`)으로 채워졌으면 그 값을, 아니면 `{param}` placeholder 를 쓴다. + * + * @param array $route 라우트 메타데이터 + * @param array $request FormRequest 분석 결과 + * @param array $probeMeta 실측 메타 (base_url, resolved_uri) + * @return string 마크다운 코드블록 + */ + public function requestExampleBlock(array $route, array $request, array $probeMeta): string + { + $method = strtoupper((string) $route['method']); + $path = (string) ($probeMeta['resolved_uri'] ?? $route['uri']); + + // GET/DELETE 의 query 파라미터는 URL 쿼리스트링으로 반영한다(바디가 아니라 URL). + if (in_array($method, ['GET', 'DELETE'], true)) { + $path = $this->appendQueryString($path, $request); + } + + // 요청 라인 + 헤더로 raw HTTP 요청 예시를 조립한다 (응답 예시의 HTTP/1.1 상태줄과 대칭). + // Host 는 실측 기준 URL(로컬 호스트)을 노출하지 않고 공개 placeholder 로 마스킹한다. + $lines = ["{$method} {$path} HTTP/1.1"]; + $lines[] = 'Host: '.self::DOC_HOST; + $lines[] = 'Accept: application/json'; + + // 인증 필요 시 Bearer 토큰 마스킹 (auth:sanctum 필수 / optional.sanctum 선택). + $mw = $route['middleware'] ?? []; + if ($this->hasMiddleware($mw, 'sanctum')) { + $optional = $this->hasMiddleware($mw, 'optional.sanctum'); + $suffix = $optional ? ' (optional.sanctum: 비회원은 헤더 생략 가능)' : ''; + $lines[] = 'Authorization: Bearer '.self::TOKEN_PLACEHOLDER.$suffix; + } + + // 바디 있는 메서드(POST/PUT/PATCH)는 바디를 붙인다. + if (in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + $bodyParams = $this->bodyParams($request); + + if ($bodyParams !== []) { + // 파일 업로드(image/file 타입) 파라미터가 있으면 multipart/form-data 로 표기한다 + // (application/json 으로는 파일을 전송할 수 없다). + if ($this->hasFileParam($bodyParams)) { + foreach ($this->multipartBodyLines($bodyParams) as $bl) { + $lines[] = $bl; + } + } else { + $lines[] = 'Content-Type: application/json'; + $lines[] = ''; + $body = $this->jsonBody($bodyParams); + $json = json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $lines[] = (string) $json; + } + } + } + + return "```http\n".implode("\n", $lines)."\n```"; + } + + /** + * GET/DELETE 의 query 파라미터를 URL 쿼리스트링으로 붙입니다. + * + * 이미 쿼리(`?per_page=..`)가 있으면 `&` 로 잇고, 각 파라미터는 타입별 대표값으로 채웁니다. + * path 파라미터는 URL 세그먼트라 제외합니다. + * + * @param string $path 경로(치환된 path 파라미터 포함 가능) + * @param array $request FormRequest 분석 결과 + * @return string 쿼리스트링이 붙은 경로 + */ + private function appendQueryString(string $path, array $request): string + { + $params = $request['params'] ?? []; + if ($params === []) { + return $path; + } + + $pathParams = []; + preg_match_all('/\{([^}]+)\}/', $path, $m); + if (! empty($m[1])) { + $pathParams = $m[1]; + } + + $pairs = []; + foreach ($params as $p) { + $name = (string) $p['name']; + if (in_array($name, $pathParams, true)) { + continue; + } + // 허용값 in: 열거가 있으면 그 첫 값을, 없으면 이름·타입 기반 대표값을 쓴다. + $value = $this->exampleValue($name, (string) ($p['type'] ?? 'string'), (string) ($p['allowed'] ?? '')); + if (is_array($value)) { + $value = $value[0] ?? ''; + } + $pairs[] = $name.'='.rawurlencode((string) $value); + } + + if ($pairs === []) { + return $path; + } + + return $path.(str_contains($path, '?') ? '&' : '?').implode('&', $pairs); + } + + /** + * body 위치 파라미터 목록을 반환합니다 (전체 — 필수+선택). + * + * @param array $request FormRequest 분석 결과 + * @return array> body 파라미터 목록 + */ + private function bodyParams(array $request): array + { + return array_values($request['params'] ?? []); + } + + /** + * 파라미터 목록에 파일(image/file) 타입이 있는지 판별합니다. + * + * @param array> $params 파라미터 목록 + * @return bool 파일 파라미터 존재 여부 + */ + private function hasFileParam(array $params): bool + { + foreach ($params as $p) { + if (in_array((string) ($p['type'] ?? ''), ['file', 'image'], true)) { + return true; + } + } + + return false; + } + + /** + * multipart/form-data 요청 예시의 헤더 + 파트 라인들을 생성합니다. + * + * 파일 파라미터는 `filename=` + `Content-Type` 파트로, 그 외는 값 파트로 표현합니다. + * + * @param array> $params body 파라미터 목록 + * @return array multipart 요청 라인들 + */ + private function multipartBodyLines(array $params): array + { + $boundary = '----G7ExampleBoundary'; + $lines = ["Content-Type: multipart/form-data; boundary={$boundary}", '']; + + foreach ($params as $p) { + $name = (string) $p['name']; + $type = (string) ($p['type'] ?? 'string'); + $lines[] = "--{$boundary}"; + + if (in_array($type, ['file', 'image'], true)) { + // 배열 파일(files[]) 은 name 뒤에 [] 를 붙인다. + $isArrayFile = str_ends_with($name, 's') || $name === 'files'; + $field = $isArrayFile ? $name.'[]' : $name; + $fileName = $type === 'image' ? 'example.png' : 'example.pdf'; + $mime = $type === 'image' ? 'image/png' : 'application/octet-stream'; + $lines[] = "Content-Disposition: form-data; name=\"{$field}\"; filename=\"{$fileName}\""; + $lines[] = "Content-Type: {$mime}"; + $lines[] = ''; + $lines[] = '(바이너리 파일 내용)'; + } else { + $lines[] = "Content-Disposition: form-data; name=\"{$name}\""; + $lines[] = ''; + $lines[] = (string) $this->exampleScalar($name, $type); + } + } + + $lines[] = "--{$boundary}--"; + + return $lines; + } + + /** + * JSON 요청 바디 맵을 조립합니다 (전체 파라미터 — 필수+선택). + * + * 값은 이름·타입에 맞는 현실적인 예시값으로 채웁니다(placeholder `"string"` 남발 방지). + * + * @param array> $params body 파라미터 목록 + * @return array 예시 바디 (이름 => 예시값) + */ + private function jsonBody(array $params): array + { + $body = []; + foreach ($params as $p) { + $name = (string) $p['name']; + $type = (string) ($p['type'] ?? 'string'); + $body[$name] = $this->exampleValue($name, $type, (string) ($p['allowed'] ?? '')); + } + + return $body; + } + + /** + * 파라미터의 예시값을 이름·타입·허용값 근거로 생성합니다. + * + * `array` 타입은 빈 배열이 아니라 대표 원소 1개를, boolean/number 는 타입값을, + * 그 외는 이름 기반 현실적 문자열(email/password/url 등)을 반환합니다. + * 허용값에 `in:` 목록(백틱)이 있으면 그 첫 값을 채택합니다. + * + * @param string $name 파라미터명 + * @param string $type 타입 + * @param string $allowed 허용값 설명 + * @return mixed 예시값 + */ + private function exampleValue(string $name, string $type, string $allowed = ''): mixed + { + // 허용값에 in: 열거(백틱으로 감싼 첫 값)가 있으면 그것을 우선 채택한다. + if (preg_match('/`([^`]+)`/', $allowed, $m)) { + return $m[1]; + } + + return match ($type) { + 'array' => [$this->exampleScalar($this->singularName($name), 'string')], + default => $this->exampleScalar($name, $type), + }; + } + + /** + * 스칼라 파라미터의 예시값을 이름·타입 근거로 생성합니다. + * + * @param string $name 파라미터명 + * @param string $type 타입 + * @return mixed 예시 스칼라값 + */ + private function exampleScalar(string $name, string $type): mixed + { + $lower = strtolower($name); + + // 타입 우선 판정 + if ($type === 'boolean') { + return true; + } + if (in_array($type, ['integer', 'number'], true)) { + return match (true) { + str_contains($lower, 'page') => 1, + str_ends_with($lower, '_id'), $lower === 'id' => 1, + default => 1, + }; + } + if ($type === 'email' || str_contains($lower, 'email')) { + return 'user@example.com'; + } + if ($type === 'uuid') { + return '9f8b2c1a-4d3e-4a2b-8c1d-0e1f2a3b4c5d'; + } + if ($type === 'date') { + return '2026-01-01'; + } + + // 이름 기반 현실적 값 + return match (true) { + str_contains($lower, 'password') => 'Password123!', + str_contains($lower, 'url') || str_contains($lower, 'homepage') => 'https://example.com', + str_contains($lower, 'identifier') || str_contains($lower, 'slug') => 'example-key', + str_contains($lower, 'name') => '예시 이름', + str_contains($lower, 'title') => '예시 제목', + str_contains($lower, 'mobile') || str_contains($lower, 'phone') => '010-1234-5678', + str_contains($lower, 'locale') || $lower === 'language' => 'ko', + str_contains($lower, 'country') => 'KR', + str_contains($lower, 'timezone') => 'Asia/Seoul', + str_contains($lower, 'zipcode') || str_contains($lower, 'postal') => '06234', + str_contains($lower, 'address') => '서울특별시 강남구 테헤란로 1', + str_contains($lower, 'content') || str_contains($lower, 'description') || str_contains($lower, 'bio') => '예시 내용입니다.', + str_contains($lower, 'token') => self::TOKEN_PLACEHOLDER, + str_contains($lower, 'color') => '#4F46E5', + default => '예시값', + }; + } + + /** + * 배열 파라미터명의 단수형 근사값을 반환합니다 (예시 원소 이름용). + * + * @param string $name 파라미터명 (복수형 가능) + * @return string 단수형 근사 + */ + private function singularName(string $name): string + { + return Str::singular($name); + } + + /** + * 실측 응답 body 전문(envelope 통짜)을 예시 블록으로 방출합니다. + * + * 실측 body 가 있으면 `{success, data, message, error}` envelope 를 pretty JSON 으로 출력합니다. + * 목록 응답의 `data.data[]` 는 대표 항목으로 절단하고 나머지는 절단 주석으로 대체합니다. + * 실측 제외(쓰기 메서드·바이너리·미치환 path)면 사람 보강 마커를 남깁니다. + * + * @param array|null $schema 실측 응답 스키마 (null=실측 안 됨) + * @param array $probeMeta 실측 메타 (status, body, skipped_reason) + * @return string 마크다운 코드블록 또는 실측 제외 마커 + */ + public function responseExampleBlock(?array $schema, array $probeMeta): string + { + $body = $probeMeta['body'] ?? null; + + if (! is_array($body)) { + $reason = $probeMeta['skipped_reason'] ?? 'not-probed'; + + return ""; + } + + $status = $probeMeta['status'] ?? 200; + $truncated = $this->truncateListBody($body); + // 실측 응답에 섞여 나온 민감값(토큰·비밀번호·시크릿 등)을 마스킹한다. + $sanitized = $this->sanitizeSensitive($truncated); + // 응답 body 내부의 절대 URL(페이지네이터 링크·콜백 URL 등)에 실측 기준 호스트가 + // 그대로 직렬화돼 들어올 수 있으므로 공개 문서용 placeholder 호스트로 마스킹한다. + $masked = $this->maskResponseHost($sanitized, $probeMeta['base_url'] ?? null); + $json = json_encode($masked, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + return "```http\nHTTP/1.1 {$status}\n```\n\n```json\n".(string) $json."\n```"; + } + + /** + * 응답 body 의 절대 URL 에 포함된 실측 기준 호스트를 공개 placeholder 호스트로 마스킹합니다 (재귀). + * + * Laravel 페이지네이터 메타(`first_page_url`/`last_page_url`/`path`/`url` 등)와 콜백/통보 URL 은 + * 실측 시 요청 base URL 의 호스트(로컬 개발 호스트 등)를 그대로 직렬화한다. 요청 예시의 `Host:` + * 헤더는 {@see self::DOC_HOST} 로 마스킹되지만 응답 body 문자열값은 별도 처리가 없으면 유출된다. + * base URL 의 scheme+host 를 `https://{DOC_HOST}` 로 치환해 응답 예시에서도 호스트를 노출하지 않는다. + * + * @param mixed $value 응답 값 (배열/스칼라) + * @param string|null $baseUrl 실측 요청 base URL (없으면 마스킹 생략) + * @return mixed 호스트가 마스킹된 값 + */ + private function maskResponseHost(mixed $value, ?string $baseUrl): mixed + { + if ($baseUrl === null || $baseUrl === '') { + return $value; + } + + $host = parse_url($baseUrl, PHP_URL_HOST); + if (! is_string($host) || $host === '' || $host === self::DOC_HOST) { + return $value; + } + + if (is_string($value)) { + // scheme://host 조합을 placeholder 로 치환 (포트 유무·http/https 모두 대응). + return preg_replace( + '#https?://'.preg_quote($host, '#').'(:\d+)?#i', + 'https://'.self::DOC_HOST, + $value + ); + } + + if (is_array($value)) { + $result = []; + foreach ($value as $k => $v) { + $result[$k] = $this->maskResponseHost($v, $baseUrl); + } + + return $result; + } + + return $value; + } + + /** + * 응답 body 에서 민감한 필드 값을 placeholder 로 마스킹합니다 (재귀). + * + * 실측 응답에 실제 토큰·비밀번호 해시·시크릿·API 키가 섞여 나올 수 있으므로 공개 문서에 + * 방출하기 전에 마스킹한다. 필드명(키)에 민감 키워드가 포함되면 값을 placeholder 로 대체. + * + * @param mixed $value 응답 값 (배열/스칼라) + * @return mixed 마스킹된 값 + */ + private function sanitizeSensitive(mixed $value): mixed + { + if (! is_array($value)) { + return $value; + } + + $sensitiveKeys = [ + 'token', 'access_token', 'refresh_token', 'plaintexttoken', 'plain_text_token', + 'password', 'secret', 'api_key', 'apikey', 'private_key', 'client_secret', + 'authorization', 'remember_token', 'app_key', + ]; + + $result = []; + foreach ($value as $k => $v) { + $keyLower = is_string($k) ? strtolower($k) : ''; + $isSensitive = false; + foreach ($sensitiveKeys as $needle) { + if ($keyLower !== '' && str_contains($keyLower, $needle)) { + $isSensitive = true; + break; + } + } + + if ($isSensitive && ! is_array($v) && $v !== null) { + // 타입 표기용 접미(_type/_expires 등)는 마스킹 대상이 아니므로 스칼라만 마스킹. + $result[$k] = ($keyLower === 'token_type') ? $v : '{MASKED}'; + } else { + $result[$k] = $this->sanitizeSensitive($v); + } + } + + return $result; + } + + /** + * 목록 응답 body 의 `data.data[]` 를 대표 항목으로 절단합니다. + * + * 목록 body 는 클 수 있으므로 최대 LIST_EXAMPLE_LIMIT 항목만 남기고 + * 나머지는 `// ... (총 N건 중 K건 표시)` 절단 주석 항목으로 대체합니다. + * + * @param array $body 실측 응답 body (envelope) + * @return array 절단된 body + */ + private function truncateListBody(array $body): array + { + $data = $body['data'] ?? null; + + if (is_array($data) && isset($data['data']) && is_array($data['data'])) { + $rows = $data['data']; + $total = count($rows); + + if ($total > self::LIST_EXAMPLE_LIMIT) { + $kept = array_slice($rows, 0, self::LIST_EXAMPLE_LIMIT); + $kept[] = "... (총 {$total}건 중 ".self::LIST_EXAMPLE_LIMIT.'건 표시)'; + $body['data']['data'] = $kept; + } + } + + return $body; + } + /** * 마크다운 표 셀 안에서 안전하도록 파이프/개행을 이스케이프합니다. * @@ -354,4 +837,266 @@ class ApiDocScaffolder return str_replace($stub, $preserved, $section); } + + /** + * 기존 문서의 각 엔드포인트 `@generated` 블록에 요청/응답 예시 블록을 in-place 삽입합니다. + * + * 전체 재생성(`endpointSection` + `mergeDocument`)은 `@generated` 블록 내부의 + * 파라미터/응답 필드 표를 통째로 다시 조립하므로, 정적 추출로 재현 불가능한 사람의 + * 도메인 서술 셀이 TODO 로 되돌아갑니다. 이 메서드는 표를 건드리지 않고 예시 2블록만 + * 표 뒤에 삽입/치환하여 그 퇴행 없이 예시를 방출합니다. + * + * 삽입 위치(스캐폴더 `endpointSection` 순서와 동일): + * - `**요청 예시**` → `**응답 필드**` 헤딩 바로 앞 + * - `**응답 예시**` → `**에러 응답**` 헤딩 바로 앞 + * + * 이미 예시 블록이 있으면(멱등) 그 블록만 새 내용으로 치환합니다. + * + * @param string $content 기존 문서 내용 + * @param array $exampleBlocks 라우트명 키 => 예시 블록 + * @return array{0: string, 1: int} [갱신된 문서, 삽입/치환한 예시 블록 수] + */ + public function insertExampleBlocks(string $content, array $exampleBlocks): array + { + $inserted = 0; + + foreach ($exampleBlocks as $key => $blocks) { + $startMarker = self::GEN_START.$key.' -->'; + $startPos = strpos($content, $startMarker); + if ($startPos === false) { + continue; + } + + $endPos = strpos($content, self::GEN_END, $startPos); + if ($endPos === false) { + continue; + } + + $block = substr($content, $startPos, $endPos - $startPos); + + // 이 엔드포인트의 사람 서술 영역(@generated:end 뒤 ~ 다음 ### 전)에 이미 사람이 작성한 + // `**응답 예시**` 가 있으면, 블록 안에 응답 예시 마커를 넣지 않는다(중복 헤딩 회피). + // 요청 예시는 항상 삽입한다. + $proseHasResponseExample = $this->proseHasResponseExample($content, $endPos); + + // 응답 필드 표가 실측 제외 마커(사람이 채운 셀 없음)이고 이번에 실측 표가 확보됐으면 + // 그 마커를 실측 필드 표로 갱신한다(쓰기 메서드 실측 도입분). 이미 필드 표(| 필드 | 타입 |)가 + // 있으면 건드리지 않아 사람 서술 셀을 보존한다. + $updatedBlock = $block; + + // 요청 파라미터 표에서 path 행과 이름이 겹치는 query 행(자동 생성 중복)을 제거한다. + // 라우트 바인딩 세그먼트가 FormRequest rules() 에도 있어 path/query 로 2회 출력되던 결함. + $updatedBlock = $this->dedupeParamRows($updatedBlock, $inserted); + + if (! empty($blocks['response_fields'])) { + $updatedBlock = $this->refreshResponseFieldMarker($updatedBlock, $blocks['response_fields'], $inserted); + } + + $updatedBlock = $this->applyExampleBlock($updatedBlock, '**요청 예시**', '**응답 필드**', $blocks['request'], $inserted); + + if ($proseHasResponseExample) { + // 서술 영역에 수기 응답 예시가 있으면 블록 안 응답 예시는 두지 않는다(중복 헤딩 회피). + // 과거 run 이 삽입한 블록 내 응답 예시가 남아 있으면 제거한다. + $updatedBlock = $this->removeExampleBlock($updatedBlock, '**응답 예시**', '**에러 응답**', $inserted); + } else { + $updatedBlock = $this->applyExampleBlock($updatedBlock, '**응답 예시**', '**에러 응답**', $blocks['response'], $inserted); + } + + if ($updatedBlock !== $block) { + $content = substr($content, 0, $startPos).$updatedBlock.substr($content, $endPos); + } + } + + return [$content, $inserted]; + } + + /** + * 엔드포인트의 사람 서술 영역에 이미 사람이 작성한 `**응답 예시**` 가 있는지 확인합니다. + * + * 서술 영역 = `@generated:end` 뒤부터 다음 `### ` 헤딩(또는 문서 끝) 전까지. 여기에 + * `**응답 예시**` 가 있으면 블록 안에 응답 예시를 삽입하지 않아 중복 헤딩을 방지한다. + * 파일 업로드(바이너리)·미설치 정적 예시처럼 실측 불가라 사람이 미리 채운 응답 예시가 대상. + * + * @param string $content 전체 문서 내용 + * @param int $endPos 이 엔드포인트 `@generated:end` 마커의 시작 위치 + * @return bool 서술 영역에 수기 응답 예시가 있으면 true + */ + private function proseHasResponseExample(string $content, int $endPos): bool + { + $afterGen = substr($content, $endPos + strlen(self::GEN_END)); + + // 다음 ### 헤딩 전까지가 이 엔드포인트의 사람 서술 영역 + $nextHeading = preg_match('/\n### /', $afterGen, $m, PREG_OFFSET_CAPTURE) + ? $m[0][1] + : strlen($afterGen); + + $prose = substr($afterGen, 0, $nextHeading); + + return Str::contains($prose, '**응답 예시**'); + } + + /** + * 요청 파라미터 표에서 path 행과 이름이 겹치는 query 행(자동 생성 중복)을 제거합니다. + * + * 라우트 바인딩 세그먼트가 FormRequest rules() 에도 존재하면 같은 이름이 `| 이름 | path |` 과 + * `| 이름 | query |` 로 2회 출력되던 결함을 in-place 로 정정한다. path 행을 남기고 query 행만 제거. + * + * @param string $block `@generated` 블록 문자열 + * @param int $inserted 정정 카운터 (참조 누적) + * @return string 정정된 블록 + */ + private function dedupeParamRows(string $block, int &$inserted): string + { + $lines = explode("\n", $block); + + // path 행의 파라미터명 수집 (| name | path | ...) + $pathNames = []; + foreach ($lines as $line) { + if (preg_match('/^\|\s*([a-zA-Z_0-9]+)\s*\|\s*path\s*\|/', $line, $m)) { + $pathNames[$m[1]] = true; + } + } + + if ($pathNames === []) { + return $block; + } + + $out = []; + $removed = false; + foreach ($lines as $line) { + // 같은 이름의 query 행이면 제거 (path 행이 우선). + if (preg_match('/^\|\s*([a-zA-Z_0-9]+)\s*\|\s*query\s*\|/', $line, $m) && isset($pathNames[$m[1]])) { + $removed = true; + + continue; + } + $out[] = $line; + } + + if ($removed) { + $inserted++; + } + + return implode("\n", $out); + } + + /** + * 응답 필드 표가 실측 제외 마커일 때만 실측 필드 표로 치환합니다. + * + * `**응답 필드** (\`data\` 내부)` 헤딩 ~ 다음 헤딩(`**응답 예시**`/`**에러 응답**`) 사이 본문이 + * `` 마커면 실측 표로 교체하고, 이미 표(`| 필드 | 타입 |`)가 있으면 + * 사람 서술 셀 보존을 위해 건드리지 않는다. + * + * @param string $block `@generated` 블록 문자열 + * @param string $fieldTable 실측 응답 필드 표 + * @param int $inserted 치환 카운터 (참조 누적) + * @return string 갱신된 블록 + */ + private function refreshResponseFieldMarker(string $block, string $fieldTable, int &$inserted): string + { + $label = '**응답 필드** (`data` 내부)'; + $labelPos = strpos($block, $label); + if ($labelPos === false) { + return $block; + } + + $bodyStart = $labelPos + strlen($label); + // 다음 헤딩(응답 예시 또는 에러 응답) 전까지가 응답 필드 본문 + $nextRespExample = strpos($block, '**응답 예시**', $bodyStart); + $nextError = strpos($block, '**에러 응답**', $bodyStart); + $candidates = array_filter([$nextRespExample, $nextError], fn ($v) => $v !== false); + if ($candidates === []) { + return $block; + } + $bodyEnd = min($candidates); + + $body = substr($block, $bodyStart, $bodyEnd - $bodyStart); + + // 사람이 채운 표(| 필드 | 타입 |)가 이미 있으면 보존 — 마커일 때만 교체. + if (! str_contains($body, '실측 제외')) { + return $block; + } + + $inserted++; + + return substr($block, 0, $bodyStart)."\n\n".$fieldTable."\n\n".substr($block, $bodyEnd); + } + + /** + * `@generated` 블록 문자열에 예시 블록 하나를 삽입하거나 치환합니다. + * + * `$label`(예: `**요청 예시**`) 섹션이 이미 있으면 그 본문을 `$body` 로 치환하고, + * 없으면 `$anchor`(예: `**응답 필드**`) 헤딩 바로 앞에 `label + body` 를 삽입합니다. + * + * @param string $block `@generated` 블록 문자열 + * @param string $label 예시 섹션 헤딩 (`**요청 예시**` / `**응답 예시**`) + * @param string $anchor 삽입 기준 헤딩 (`**응답 필드**` / `**에러 응답**`) + * @param string $body 예시 블록 본문 (코드블록 또는 실측 제외 마커) + * @param int $inserted 삽입/치환 카운터 (참조 누적) + * @return string 갱신된 블록 문자열 + */ + private function applyExampleBlock(string $block, string $label, string $anchor, string $body, int &$inserted): string + { + $section = $label."\n\n".$body."\n\n"; + $labelLine = $label."\n"; + + // 이미 예시 섹션이 있으면 그 label ~ (다음 빈 줄 + 다음 헤딩) 까지를 새 섹션으로 치환한다. + $labelPos = strpos($block, $labelLine); + if ($labelPos !== false) { + $anchorPos = strpos($block, $anchor, $labelPos); + if ($anchorPos === false) { + return $block; + } + + $replaced = substr($block, 0, $labelPos).$section.substr($block, $anchorPos); + if ($replaced !== $block) { + $inserted++; + } + + return $replaced; + } + + // 없으면 anchor 헤딩 바로 앞에 삽입. + $anchorPos = strpos($block, $anchor); + if ($anchorPos === false) { + return $block; + } + + $inserted++; + + return substr($block, 0, $anchorPos).$section.substr($block, $anchorPos); + } + + /** + * `@generated` 블록 문자열에서 예시 섹션 하나를 제거합니다. + * + * 서술 영역에 수기 예시가 있어 블록 안 예시가 중복이 되는 경우, `$label` 섹션 + * (`$label` ~ `$anchor` 직전)을 제거한다. 섹션이 없으면 그대로 반환한다. + * + * @param string $block `@generated` 블록 문자열 + * @param string $label 제거할 예시 섹션 헤딩 (`**응답 예시**`) + * @param string $anchor 섹션 끝 기준 헤딩 (`**에러 응답**`) + * @param int $inserted 변경 카운터 (참조 누적) + * @return string 갱신된 블록 문자열 + */ + private function removeExampleBlock(string $block, string $label, string $anchor, int &$inserted): string + { + $labelLine = $label."\n"; + $labelPos = strpos($block, $labelLine); + if ($labelPos === false) { + return $block; + } + + $anchorPos = strpos($block, $anchor, $labelPos); + if ($anchorPos === false) { + return $block; + } + + $removed = substr($block, 0, $labelPos).substr($block, $anchorPos); + if ($removed !== $block) { + $inserted++; + } + + return $removed; + } } diff --git a/app/Support/ApiDoc/ApiEndpointProbe.php b/app/Support/ApiDoc/ApiEndpointProbe.php index 286fa4d8..a50d8d4e 100644 --- a/app/Support/ApiDoc/ApiEndpointProbe.php +++ b/app/Support/ApiDoc/ApiEndpointProbe.php @@ -3,6 +3,9 @@ namespace App\Support\ApiDoc; use App\Models\User; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; use Laravel\Sanctum\PersonalAccessToken; @@ -31,6 +34,11 @@ class ApiEndpointProbe */ private string $tokenName = 'api-docgen-probe'; + /** + * @var User|null 실측 인증 사용자 (in-process 쓰기 실측 시 로그인 대상) + */ + private ?User $user = null; + /** * @param string|null $baseUrl 기준 URL (null 이면 .env 의 APP_URL 직접 사용) */ @@ -71,6 +79,7 @@ class ApiEndpointProbe return false; } + $this->user = $user; $this->cleanupTokens(); $this->token = $user->createToken($this->tokenName)->plainTextToken; @@ -78,21 +87,21 @@ class ApiEndpointProbe } /** - * GET 엔드포인트를 실호출하여 응답을 관측합니다. + * 엔드포인트를 실호출하여 응답을 관측합니다. + * + * GET/HEAD 는 외부 HTTP 로 read-only 실측하고, 쓰기 메서드(POST/PUT/PATCH/DELETE)는 + * **DB 트랜잭션 안에서 in-process 로 dispatch 한 뒤 롤백**하여(영속 안 함) 응답 shape 만 + * 관측한다. 외부 HTTP 는 별도 프로세스라 롤백이 불가하므로 쓰기는 in-process 경로를 쓴다. * * @param string $method HTTP 메서드 * @param string $uri 라우트 URI (path 파라미터 치환 완료된 실제 경로) + * @param array $body 쓰기 요청 바디 (쓰기 메서드에서만 사용) * @return array{ok: bool, status: int|null, body: array|null, skipped_reason: string|null} */ - public function probe(string $method, string $uri): array + public function probe(string $method, string $uri, array $body = []): array { $method = strtoupper($method); - // 쓰기 메서드는 부수효과 위험으로 실호출하지 않는다 (정적 문서화로 대체). - if (! in_array($method, ['GET', 'HEAD'], true)) { - return ['ok' => false, 'status' => null, 'body' => null, 'skipped_reason' => 'write-method']; - } - // path 파라미터가 남아 있으면(치환 실패) 실호출 불가. if (Str::contains($uri, '{')) { return ['ok' => false, 'status' => null, 'body' => null, 'skipped_reason' => 'unresolved-path-param']; @@ -102,6 +111,22 @@ class ApiEndpointProbe return ['ok' => false, 'status' => null, 'body' => null, 'skipped_reason' => 'no-token']; } + if (in_array($method, ['GET', 'HEAD'], true)) { + return $this->probeRead($method, $uri); + } + + return $this->probeWrite($method, $uri, $body); + } + + /** + * GET/HEAD 를 외부 HTTP 로 read-only 실측합니다. + * + * @param string $method HTTP 메서드 + * @param string $uri 치환된 URI + * @return array{ok: bool, status: int|null, body: array|null, skipped_reason: string|null} + */ + private function probeRead(string $method, string $uri): array + { try { $response = Http::withoutVerifying() ->withToken($this->token) @@ -122,6 +147,54 @@ class ApiEndpointProbe } } + /** + * 쓰기 메서드를 DB 트랜잭션 안에서 in-process dispatch 후 롤백하여 실측합니다. + * + * 응답 shape 만 관측하고 부수효과(레코드 생성/수정/삭제)는 롤백으로 영속시키지 않는다. + * 인증은 샘플 사용자로 로그인(guard)해 수행하며, DELETE 등 바디 없는 메서드도 처리한다. + * + * @param string $method HTTP 메서드 + * @param string $uri 치환된 URI + * @param array $body 요청 바디 + * @return array{ok: bool, status: int|null, body: array|null, skipped_reason: string|null} + */ + private function probeWrite(string $method, string $uri, array $body): array + { + if (! $this->user) { + return ['ok' => false, 'status' => null, 'body' => null, 'skipped_reason' => 'no-token']; + } + + DB::beginTransaction(); + + try { + // in-process dispatch 는 실제 Bearer 토큰 헤더로 인증한다(guard 종류 무관). + // Auth::login() 은 API 기본 guard 가 RequestGuard(토큰 전용)라 사용할 수 없다. + $request = Request::create($uri, $method, $body); + $request->headers->set('Accept', 'application/json'); + $request->headers->set('Authorization', 'Bearer '.$this->token); + + $response = app()->handle($request); + $content = $response->getContent(); + $json = json_decode(is_string($content) ? $content : '', true); + $status = $response->getStatusCode(); + + return [ + 'ok' => $status >= 200 && $status < 300 && is_array($json), + 'status' => $status, + 'body' => is_array($json) ? $json : null, + 'skipped_reason' => (is_array($json) && $status >= 200 && $status < 300) ? null : 'http-'.$status, + ]; + } catch (\Throwable $e) { + return ['ok' => false, 'status' => null, 'body' => null, 'skipped_reason' => 'write-probe-failed: '.$e->getMessage()]; + } finally { + // 쓰기 부수효과를 영속시키지 않는다 (문서 생성 목적). + DB::rollBack(); + // 다음 실측에 인증 컨텍스트가 새지 않도록 해석된 guard 를 초기화한다 + // (API guard 는 RequestGuard 라 logout() 이 없으므로 forgetGuards 사용). + Auth::forgetGuards(); + } + } + /** * 발급한 임시 토큰을 정리합니다. */ diff --git a/app/Support/ApiDoc/ApiRouteInventory.php b/app/Support/ApiDoc/ApiRouteInventory.php index aaa27a0e..001b3173 100644 --- a/app/Support/ApiDoc/ApiRouteInventory.php +++ b/app/Support/ApiDoc/ApiRouteInventory.php @@ -143,13 +143,6 @@ class ApiRouteInventory $urlPrefix = "api/{$dir}/{$id}"; $namePrefix = "api.{$dir}.{$id}."; - // 등록 전 스냅샷 — 이미 등록된(활성) 라우트와 이번 로드분을 구분한다. - $existing = []; - foreach (RouteFacade::getRoutes() as $route) { - /** @var Route $route */ - $existing[spl_object_id($route)] = true; - } - try { RouteFacade::prefix($urlPrefix) ->name($namePrefix) @@ -160,14 +153,15 @@ class ApiRouteInventory return []; } + // 이 확장 소유로 확정된 라우트를 전역 라우트 테이블에서 prefix(uri/name) 로 직접 골라낸다. + // object-id 스냅샷 방식은 refreshNameLookups() 가 RouteCollection 을 재색인하며 객체를 + // 다시 만들어 dedup 이 어긋나므로(활성 확장 로드 순서·중복 로드에 취약) 쓰지 않는다. + // 대상 확장은 uri 가 `api/{dir}/{id}/`, name 이 `api.{dir}.{id}.` 로 시작해 결정적으로 식별된다. + $seen = []; $routes = []; foreach (RouteFacade::getRoutes() as $route) { /** @var Route $route */ - if (isset($existing[spl_object_id($route)])) { - continue; - } - $uri = $route->uri(); if (! Str::startsWith($uri, 'api/')) { @@ -177,12 +171,18 @@ class ApiRouteInventory $name = $route->getName() ?? ''; $owner = $this->resolveOwner($name, $uri); - // 폴백 대상 확장 소유로 확정되지 않으면(코어 등) 제외 + // 폴백 대상 확장 소유로 확정되지 않으면(코어·타 확장) 제외 if ($owner['key'] !== $scope) { continue; } foreach ($this->normalizeRoute($route, $uri, $name, $owner) as $entry) { + // 파일이 이미 한 번 로드돼 있으면 같은 (method, uri) 가 중복될 수 있어 dedup 한다. + $dedupKey = $entry['method'].' '.$entry['uri']; + if (isset($seen[$dedupKey])) { + continue; + } + $seen[$dedupKey] = true; $routes[] = $entry; } } diff --git a/app/Support/ApiDoc/ResourceFieldDescriber.php b/app/Support/ApiDoc/ResourceFieldDescriber.php index 191e9254..c11963d3 100644 --- a/app/Support/ApiDoc/ResourceFieldDescriber.php +++ b/app/Support/ApiDoc/ResourceFieldDescriber.php @@ -103,6 +103,38 @@ class ResourceFieldDescriber 'origin' => '출처 (설치/등록 원천 구분 값)', 'target_name' => '대상 확장의 표시 이름 (scope+target_identifier 로 해석)', 'install_blocked_reason' => '설치가 차단된 사유 (차단 없으면 null)', + + // 감사/관계 공통 (도메인 무관하게 역할 고정 — 쓰기 응답에서 흔히 등장) + 'updated_by' => '최종 수정한 사용자 정보 (uuid/name — updated_by 관계 파생, 없으면 null)', + 'creator' => '생성자 정보 객체 (uuid/name/email — creator 관계 파생)', + 'user' => '대상 사용자 정보 객체 (uuid/name/email 등 — user 관계 파생)', + 'roles' => '보유 역할 목록 (각 원소 id/name/permissions — roles 관계 파생)', + 'permissions' => '권한 목록 (각 원소 identifier/name — permissions 관계 파생)', + 'avatar' => '아바타 이미지 URL (미등록 시 null)', + + // 인증/토큰 (로그인/토큰 발급 응답) + 'token' => '발급된 API 접근 토큰 평문 (Bearer 토큰으로 사용, 발급 시 1회만 노출)', + 'token_type' => '토큰 타입 (일반적으로 Bearer)', + 'trigger_type' => '동작을 유발한 방식/주체 구분 값', + + // 스케줄/실행 이력 (Schedule 실행 결과) + 'last_duration' => '마지막 실행 소요 시간 (초/밀리초 — 실행 이력 파생)', + 'duration' => '실행 소요 시간 (초/밀리초)', + 'exit_code' => '실행 종료 코드 (0=성공, 그 외=실패)', + 'output' => '실행 표준 출력 내용', + 'error_output' => '실행 표준 에러 출력 내용 (없으면 빈 문자열/null)', + 'memory_usage' => '실행 중 최대 메모리 사용량 (바이트)', + + // 캐시/파일/집계 (일회성 동작 응답) + 'ttl' => '캐시 유효 시간 (Time To Live, 초)', + 'size_bytes' => '크기 (바이트)', + 'older_than_days' => '기준 경과 일수 (이 일수보다 오래된 대상 필터/집계)', + 'entries' => '항목 목록 (각 원소는 대상 도메인 레코드)', + 'changelog_entries' => '변경 이력 항목 목록 (버전별 변경 내용)', + 'templates' => '템플릿 목록 (각 원소 identifier/name 등 — 템플릿 관계 파생)', + 'spec' => '스펙 정의 객체 (편집기/컴포넌트 선언 스키마 등)', + 'source_meta' => '원천 메타데이터 객체 (출처·경로·해석 정보)', + 'validation_summary' => '검증 결과 요약 객체 (통과/실패 건수 등)', ]; /** @@ -123,6 +155,13 @@ class ResourceFieldDescriber : null; } + // status 는 도메인마다 허용값 집합이 다르나(active/blocked, success/failed 등) 역할은 + // 공통이다. 문자열 상태값이면 일반 설명을 주고, 구체 허용값은 status_label/변형 필드와 + // 각 도메인 문서에서 보강한다. (동반 필드 status_label/status_variant 는 EXACT 로 설명됨) + if ($field === 'status' && in_array($type, ['string', 'integer'], true)) { + return '상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조)'; + } + if (isset(self::EXACT[$field])) { return self::EXACT[$field]; } diff --git a/docs/backend/README.md b/docs/backend/README.md index ffe7cd6d..341c9790 100644 --- a/docs/backend/README.md +++ b/docs/backend/README.md @@ -23,7 +23,7 @@ | [activity-log-hooks.md](activity-log-hooks.md) | 활동 로그 훅 레퍼런스 (Activity Log Hooks Reference) | 코어 66훅 + 이커머스 92훅 + 게시판 32훅 + 페이지 8훅 = 총 198훅 | | [activity-log.md](activity-log.md) | 활동 로그 시스템 (Activity Log System) | Monolog 기반: Service 훅 → Listener → Log::channel... | | [admin-settings-access.md](admin-settings-access.md) | Admin 환경설정 값 접근 (`g7_core_settings` vs `config()`) | 동기화 SSoT: storage/app/settings/*.json → Setting... | -| [api-documentation.md](api-documentation.md) | API 레퍼런스 문서 규정 (API Documentation) | 모든 API 엔드포인트는 레퍼런스 문서 필수 — 메서드/URI/파라미터/응답 필드 전... | +| [api-documentation.md](api-documentation.md) | API 레퍼런스 문서 규정 (API Documentation) | 모든 API 엔드포인트는 레퍼런스 문서 필수 — 메서드/URI/파라미터/응답 필드 +... | | [api-resources.md](api-resources.md) | API 리소스 | Resource: BaseApiResource 상속 필수 / Collection: B... | | [authentication.md](authentication.md) | 인증 및 세션 처리 | Laravel Sanctum 토큰 전용 인증 (Bearer 토큰만 사용) | | [broadcasting.md](broadcasting.md) | Broadcasting (실시간 이벤트) | Laravel Reverb 사용 (WebSocket) | diff --git a/docs/backend/api-documentation.md b/docs/backend/api-documentation.md index 0dd00751..ae23b398 100644 --- a/docs/backend/api-documentation.md +++ b/docs/backend/api-documentation.md @@ -7,7 +7,7 @@ ## TL;DR (5초 요약) ```text -1. 모든 API 엔드포인트는 레퍼런스 문서 필수 — 메서드/URI/파라미터/응답 필드 전수 기재 +1. 모든 API 엔드포인트는 레퍼런스 문서 필수 — 메서드/URI/파라미터/응답 필드 + 요청·응답 예시 전수 기재 2. 위치: 코어 = docs/backend/api/, 확장 = {modules|plugins}/_bundled/{id}/docs/api/ 3. 생성: php artisan api:docgen — 코드에서 추출한 스캐폴딩 + 사람이 서술 보강 (순수 수기 금지) 4. 추출 불가분(훅 주입 파라미터·동적 응답)은 마커 남기고 사람이 채움 @@ -55,14 +55,48 @@ G7 의 REST API 는 라우트 `->name()` 규약은 있으나 엔드포인트별 ## 표준 문서 포맷 -엔드포인트 1개당 아래 4개 구성(헤더 · 요청 파라미터 · 응답 필드 · 에러 응답)을 따른다. -`` ~ `` 사이는 `api:docgen` 이 재생성하는 추출 블록이며, -그 바깥의 사람 서술은 재생성 시 보존된다. +엔드포인트 1개당 아래 6개 구성(헤더 · 요청 파라미터 · 요청 예시 · 응답 필드 · 응답 예시 · 에러 응답)을 +따른다. `` ~ `` 사이는 `api:docgen` 이 재생성하는 추출 +블록이며, 그 바깥의 사람 서술(`**설명**`)은 재생성 시 보존된다. 에러 응답 표는 라우트 메타에서 대표 상태코드를 자동 추론한다: 인증 필수(`auth:sanctum`)→401, `admin`/`permission:` 요구→403, FormRequest 검증 규칙 존재→422, path 파라미터 존재→404. `optional.sanctum`(선택 인증)은 401 을 유발하지 않는다. 도메인 특이 에러(409·429 등)는 사람이 보강한다. +**요청 예시**(raw HTTP 요청)와 **응답 예시**(envelope 전문 JSON)는 실제 호출을 재현할 수 있도록 실측 기반으로 +방출된다(단계 6). 요청 예시는 curl 이 아니라 raw HTTP 요청(요청 라인 + 헤더)으로 표기해 응답 예시의 +`HTTP/1.1 {status}` 상태줄과 대칭을 이룬다. 세부 규칙: + +- **요청 예시**: 요청 라인(`{METHOD} {path} HTTP/1.1`) + `Host:` + `Accept: application/json`, 인증 필요 시 + `Authorization: Bearer {YOUR_TOKEN}`(실측 토큰 평문 유출 방지 마스킹). `Host` 는 실측 기준 URL(로컬 개발 + 호스트 등)을 노출하지 않고 공개 placeholder(`api.example.com`, RFC 2606 예약 도메인)로 마스킹한다. + - **바디 메서드(POST/PUT/PATCH)**: `Content-Type: application/json` + 빈 줄 뒤 JSON 바디. 바디는 필수만이 + 아니라 **전체 파라미터**를 담고 값은 이름·타입 기반 현실적 예시값으로 채운다(`"string"` placeholder 남발 + 금지). 허용값 `in:` 열거가 있으면 그 첫 값을 채택한다. + - **파일 업로드**(요청 파라미터 타입이 `image`/`file`): `application/json` 이 아니라 + `Content-Type: multipart/form-data; boundary=...` 로 표기하고 각 파일 파트를 `filename=`+`Content-Type` + 으로 나타낸다(JSON 으로는 파일 전송 불가). + - **GET/DELETE**: query 파라미터는 URL 쿼리스트링(`?a=..&b=..`)으로 반영한다(바디 아님). + - `optional.sanctum`(선택 인증) 엔드포인트는 Authorization 헤더에 "비회원은 생략 가능" 주석을 붙인다. +- **응답 예시**: `HTTP/1.1 {status}` 상태줄 + 실측 응답 body 전문(`{success, data, message, error}` envelope). + 목록 응답의 `data.data[]` 는 대표 **2항목**으로 절단하고 나머지는 `... (총 N건 중 2건 표시)` 항목으로 대체한다. + 응답에 섞여 나온 **민감값(토큰·비밀번호·시크릿·API 키)은 `{MASKED}` 로 마스킹**해 방출한다. +- **쓰기 메서드 실측**: GET/HEAD 는 외부 HTTP 로 read-only 실측하고, 쓰기(POST/PUT/PATCH/DELETE)는 **DB 트랜잭션 + 안에서 in-process dispatch 후 롤백**하여 응답 shape 만 관측한다(부수효과 미영속). 단, 부수효과가 롤백으로 + 되돌릴 수 없는 쓰기(확장 install/activate/update, 언어팩 설치, 코어 업데이트, 파일 업로드, 캐시/워밍업/생성 + 등 파일시스템·프로세스·외부 네트워크 접촉)는 실측에서 제외(`side-effectful-write`)하고 요청 예시만 정적 방출한다. +- **실측 제외**(부수효과 쓰기·바이너리·미치환 path 파라미터 등) 엔드포인트: 요청 예시는 파라미터 표 기반으로 정적 + 조립(raw HTTP 요청 골격), 응답 예시는 `` 마커로 남긴다. +- 두 예시 블록은 전량 `@generated` 블록 **내부**에 있다. 다만 이미 파라미터/응답 필드 표의 사람 서술이 + 채워진 문서에 예시를 추가할 때는 전체 재생성(`api:docgen`)을 쓰지 않는다 — 전체 재생성은 표를 통째로 + 다시 조립해 정적 추출로 재현 불가능한 도메인 서술 셀을 TODO 로 되돌린다. 대신 `api:docgen --examples-only` + 로 표·서술을 건드리지 않고 예시 2블록만 in-place 삽입/치환한다(측정은 재수행하되 표는 불가침). 백필 + 커맨드(`backfill-params`/`backfill-fields`)도 예시 블록을 건드리지 않는다. +- **`--examples-only` 모드**: 기존 문서의 각 엔드포인트 `@generated` 블록에 요청 예시(응답 필드 표 앞)와 + 응답 예시(에러 응답 표 앞)만 삽입한다. 이미 예시가 있으면 그 블록만 새 실측으로 치환(멱등, 실측 상대시간 + 필드는 재측정으로 값이 달라질 수 있음). 표·필드 설명·엔드포인트 서술은 전량 보존된다. 최초 스캐폴딩이 + 없는 신규 문서는 먼저 `api:docgen` 으로 생성한 뒤 서술을 채우고, 이후 예시는 `--examples-only` 로 유지한다. + ```markdown ### GET /api/admin/users @@ -79,12 +113,43 @@ G7 의 REST API 는 라우트 `->name()` 규약은 있으나 엔드포인트별 > 이 엔드포인트는 확장이 파라미터를 추가할 수 있음 (`core.user.search_validation_rules`). +**요청 예시** + +​```http +GET /api/admin/users HTTP/1.1 +Host: example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +​``` + **응답 필드** (`data` 내부) -| 필드 | 타입 | 용도/설명 | -|------|------|-----------| -| id | integer | | -| uuid | string | | +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +|------|------|-------------|-----------| +| id | integer | `1` | | +| uuid | string | `a231747f-...` | | + +**응답 예시** + +​```http +HTTP/1.1 200 +​``` + +​```json +{ + "success": true, + "data": { + "data": [ + { "id": 1, "uuid": "a231747f-..." }, + { "id": 2, "uuid": "b0f2..." }, + "... (총 42건 중 2건 표시)" + ], + "pagination": { "current_page": 1, "total": 42 } + }, + "message": null, + "error": null +} +​``` **에러 응답** @@ -96,12 +161,6 @@ G7 의 REST API 는 라우트 `->name()` 규약은 있으나 엔드포인트별 **설명** - -**응답 예시** - -​```json -{ "success": true, "data": { }, "message": null, "error": null } -​``` ``` ### 응답 envelope 표준 @@ -141,6 +200,9 @@ php artisan api:docgen --scope=all # 생성 없이 누락/drift 만 리포트 (하네스가 소비) php artisan api:docgen --check +# 이미 서술이 채워진 문서에 요청/응답 예시 블록만 in-place 삽입 (표·서술 불가침, 재생성 금지) +php artisan api:docgen --scope=module:sirsoft-board --seed --base-url=https://example.dev --examples-only + # 생성될 대상만 미리보기 php artisan api:docgen --scope=core --dry-run ``` @@ -189,6 +251,25 @@ php artisan api:docgen --scope=core --dry-run - 확장에 새 PHP 클래스를 추가했으므로 `_bundled` 작업 후 `{type}:update {id} --force` 로 활성 디렉토리에 반영해야 오토로드된다. +### 설명 백필 커맨드 (`api:docgen-backfill-*`) + +파라미터/응답 필드 표의 `` 셀 중 **도메인 무관 공통 필드**(페이지네이션·정렬·검색· +식별자·기간·토글·공통 파생 필드 등)는 전체 재생성 없이 in-place 로 자동 서술한다. 전체 재생성 +(`api:docgen`)은 `@generated` 블록 표를 통째로 재조립해 실측 예시값·사람 서술 셀을 되돌리므로, TODO +셀만 치환하는 별도 백필 커맨드를 쓴다(멱등 — 채워진 셀·실측 예시값 불가침). + +```bash +# 요청 파라미터 표 TODO 셀 백필 (SSoT: App\Support\ApiDoc\ParameterDescriber) +php artisan api:docgen-backfill-params + +# 응답 필드 표 TODO 셀 백필 (SSoT: App\Support\ApiDoc\ResourceFieldDescriber) +php artisan api:docgen-backfill-fields +``` + +- 도메인 종속 필드(`status`/`type`/`code`/`category`·훅 주입·조건부 필드)는 값 의미가 도메인마다 달라 + 자동 채우지 않고 TODO 로 남겨 사람이 컨트롤러/FormRequest/Resource 근거로 서술한다. +- 새 공통 필드 규칙을 Describer 사전에 추가한 뒤 백필을 재실행하면 그 필드가 전 문서에서 자동 채워진다. + --- ## 문서 갱신 의무 @@ -212,7 +293,9 @@ php artisan api:docgen --scope=core --dry-run ```text □ 엔드포인트가 대응 위치(코어 docs/backend/api/ 또는 확장 docs/api/)에 문서화되었는가? □ 요청 파라미터 표에 위치/타입/필수/허용값/용도가 모두 기재되었는가? +□ 요청 예시(raw HTTP 요청) 블록이 방출되었는가? (curl 금지, 인증 필요 시 Bearer {YOUR_TOKEN} 마스킹) □ 응답 필드 표가 envelope 의 data 내부 기준으로 작성되었는가? +□ 응답 예시(envelope 전문 JSON) 블록이 방출되었는가? (목록은 2항목 절단) □ 훅 주입 파라미터가 있으면 주석 + 사람 보강이 되었는가? □ TODO 마커가 모두 채워졌는가? □ api:docgen --check 가 drift 0 인가? diff --git a/docs/backend/api/activity-logs.md b/docs/backend/api/activity-logs.md index 168fa3a2..4e66ebdd 100644 --- a/docs/backend/api/activity-logs.md +++ b/docs/backend/api/activity-logs.md @@ -42,6 +42,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.activity_log.index_validation_rules`). +**요청 예시** + +```http +GET /api/admin/activity-logs?log_type=%EC%98%88%EC%8B%9C%EA%B0%92&action=%EC%98%88%EC%8B%9C%EA%B0%92&user_id=1&loggable_type=%EC%98%88%EC%8B%9C%EA%B0%92&search=%EC%98%88%EC%8B%9C%EA%B0%92&search_type=%EC%98%88%EC%8B%9C%EA%B0%92&created_by=%EC%98%88%EC%8B%9C%EA%B0%92&date_from=2026-01-01&date_to=2026-01-01&per_page=1&sort_by=%EC%98%88%EC%8B%9C%EA%B0%92&sort_order=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -70,6 +79,100 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_read":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "활동 로그 정보를 성공적으로 가져왔습니다.", + "data": { + "data": [ + { + "number": 22, + "id": 22, + "log_type": "admin", + "log_type_label": "관리자", + "loggable_type": null, + "loggable_type_display": null, + "loggable_id": null, + "action": "user.index", + "action_label": "목록 조회", + "localized_description": "사용자 목록 조회", + "description_key": "activity_log.description.user_index", + "properties": { + "result_count": 2 + }, + "changes": null, + "bulk_changes": null, + "has_changes": false, + "actor_name": "API 문서 샘플 사용자", + "user": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" + }, + "ip_address": "127.0.0.1", + "created_at": "2026-07-08 12:12:12", + "is_owner": true, + "abilities": { + "can_read": true, + "can_delete": true + } + }, + { + "number": 21, + "id": 21, + "log_type": "admin", + "log_type_label": "관리자", + "loggable_type": null, + "loggable_type_display": null, + "loggable_id": null, + "action": "user.index", + "action_label": "목록 조회", + "localized_description": "사용자 목록 조회", + "description_key": "activity_log.description.user_index", + "properties": { + "result_count": 2 + }, + "changes": null, + "bulk_changes": null, + "has_changes": false, + "actor_name": "API 문서 샘플 사용자", + "user": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" + }, + "ip_address": "127.0.0.1", + "created_at": "2026-07-08 12:08:26", + "is_owner": true, + "abilities": { + "can_read": true, + "can_delete": true + } + }, + "... (총 22건 중 2건 표시)" + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 22, + "from": 1, + "to": 22, + "has_more_pages": false + }, + "abilities": { + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -95,10 +198,30 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +**요청 예시** + +```http +POST /api/admin/activity-logs/bulk-delete HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -124,9 +247,34 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | activityLog | path | string | 예 | — | 대상 activity log의 식별자 | +**요청 예시** + +```http +DELETE /api/admin/activity-logs/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "활동 로그가 삭제되었습니다.", + "data": null +} +``` **에러 응답** diff --git a/docs/backend/api/attachment.md b/docs/backend/api/attachment.md index d11536c9..ba6a2986 100644 --- a/docs/backend/api/attachment.md +++ b/docs/backend/api/attachment.md @@ -29,10 +29,22 @@ | --- | --- | --- | --- | --- | --- | | hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | +**요청 예시** + +```http +GET /api/attachment/{hash} HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/attachments.md b/docs/backend/api/attachments.md index 6b21bc66..279ef002 100644 --- a/docs/backend/api/attachments.md +++ b/docs/backend/api/attachments.md @@ -36,10 +36,51 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.attachment.upload_validation_rules`). +**요청 예시** + +```http +POST /api/admin/attachments HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary +Content-Disposition: form-data; name="attachmentable_type" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="attachmentable_id" + +1 +------G7ExampleBoundary +Content-Disposition: form-data; name="collection" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="source_type" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="source_identifier" + +example-key +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -72,10 +113,35 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.attachment.upload_batch_validation_rules`). +**요청 예시** + +```http +POST /api/admin/attachments/batch HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "files": [ + "예시값" + ], + "attachmentable_type": "예시값", + "attachmentable_id": 1, + "collection": "예시값", + "source_type": "예시값", + "source_identifier": "example-key" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -103,10 +169,30 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.attachment.reorder_validation_rules`). +**요청 예시** + +```http +PATCH /api/admin/attachments/reorder HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "order": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -132,10 +218,23 @@ | --- | --- | --- | --- | --- | --- | | attachment | path | string | 예 | — | 대상 attachment의 식별자 | +**요청 예시** + +```http +DELETE /api/admin/attachments/{attachment} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/auth.md b/docs/backend/api/auth.md index fb42d2ff..8c2880f2 100644 --- a/docs/backend/api/auth.md +++ b/docs/backend/api/auth.md @@ -27,9 +27,34 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/auth/logout HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "로그아웃이 성공했습니다.", + "data": null +} +``` **에러 응답** @@ -54,9 +79,76 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/auth/refresh HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| user | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 대상 사용자 정보 객체 (uuid/name/email 등 — user 관계 파생) | +| token | string | `75\|WgPUplvLGTv8YIj4507uIR6dEOHTXyNUed…` | 발급된 API 접근 토큰 평문 (Bearer 토큰으로 사용, 발급 시 1회만 노출) | +| token_type | string | `Bearer` | 토큰 타입 (일반적으로 Bearer) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "성공적으로 처리되었습니다.", + "data": { + "user": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "nickname": "song.hyunji", + "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": "https://example.com", + "mobile": "010-9595-2897", + "phone": "02-637-5618", + "zipcode": "16505", + "address": "경기도 안양시 봉은사로 2918", + "address_detail": "48동 718호", + "signature": "Fugit consequuntur repellendus sed.", + "bio": "Ut magni et sunt ducimus error adipisci. Pariatur corporis voluptatem ratione quo non saepe. Illo atque praesentium possimus dolores qui est fugit. Sint fugiat numquam voluptates.", + "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 + } + }, + "token": "{MASKED}", + "token_type": "Bearer" + } +} +``` **에러 응답** @@ -81,6 +173,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/auth/user HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -117,6 +218,1058 @@ _단건 응답: `data` 객체의 필드._ | is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_read":true,"can_create":true,"can_update":true,"can…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```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", + "avatar": null, + "language": "ko", + "language_label": "한국어", + "country": "KR", + "status": "active", + "status_label": "활성", + "status_variant": "success", + "is_admin": true, + "homepage": "https://example.com", + "mobile": "010-9595-2897", + "phone": "02-637-5618", + "zipcode": "16505", + "address": "경기도 안양시 봉은사로 2918", + "address_detail": "48동 718호", + "signature": "Fugit consequuntur repellendus sed.", + "bio": "Ut magni et sunt ducimus error adipisci. Pariatur corporis voluptatem ratione quo non saepe. Illo atque praesentium possimus dolores qui est fugit. Sint fugiat numquam voluptates.", + "last_login_at": "2026-07-07 10:41:24", + "email_verified_at": "2026-07-08 10:41:24", + "timezone": "Asia/Seoul", + "roles": [ + { + "id": 1, + "identifier": "admin", + "name": "관리자" + }, + { + "id": 4, + "identifier": "apidoc-sample-role", + "name": "API 문서 샘플 역할" + } + ], + "permissions": [ + { + "id": 5, + "identifier": "sirsoft-board.apidoc-sample-board.admin.posts.read", + "name": "게시글 조회 (관리자)" + }, + { + "id": 6, + "identifier": "sirsoft-board.apidoc-sample-board.admin.posts.write", + "name": "게시글 작성/수정/삭제 (관리자)" + }, + { + "id": 7, + "identifier": "sirsoft-board.apidoc-sample-board.admin.posts.read-secret", + "name": "비밀글 조회 (관리자)" + }, + { + "id": 8, + "identifier": "sirsoft-board.apidoc-sample-board.admin.comments.read", + "name": "댓글 조회 (관리자)" + }, + { + "id": 9, + "identifier": "sirsoft-board.apidoc-sample-board.admin.comments.write", + "name": "댓글 작성/수정/삭제 (관리자)" + }, + { + "id": 10, + "identifier": "sirsoft-board.apidoc-sample-board.admin.attachments.upload", + "name": "파일 업로드/삭제 (관리자)" + }, + { + "id": 11, + "identifier": "sirsoft-board.apidoc-sample-board.admin.attachments.download", + "name": "파일 다운로드 (관리자)" + }, + { + "id": 12, + "identifier": "sirsoft-board.apidoc-sample-board.admin.manage", + "name": "게시판 관리 (관리자)" + }, + { + "id": 13, + "identifier": "sirsoft-board.apidoc-sample-board.posts.read", + "name": "게시글 조회" + }, + { + "id": 14, + "identifier": "sirsoft-board.apidoc-sample-board.posts.write", + "name": "게시글 작성/수정/삭제" + }, + { + "id": 15, + "identifier": "sirsoft-board.apidoc-sample-board.posts.read-secret", + "name": "비밀글 조회" + }, + { + "id": 16, + "identifier": "sirsoft-board.apidoc-sample-board.comments.read", + "name": "댓글 조회" + }, + { + "id": 17, + "identifier": "sirsoft-board.apidoc-sample-board.comments.write", + "name": "댓글 작성/수정/삭제" + }, + { + "id": 18, + "identifier": "sirsoft-board.apidoc-sample-board.attachments.upload", + "name": "파일 업로드/삭제" + }, + { + "id": 19, + "identifier": "sirsoft-board.apidoc-sample-board.attachments.download", + "name": "파일 다운로드" + }, + { + "id": 20, + "identifier": "sirsoft-board.apidoc-sample-board.manager", + "name": "게시판 관리 (사용자)" + }, + { + "id": 81, + "identifier": "sirsoft-ecommerce.user-products.read", + "name": "상품 조회" + }, + { + "id": 83, + "identifier": "sirsoft-ecommerce.user-orders.create", + "name": "주문하기" + }, + { + "id": 84, + "identifier": "sirsoft-ecommerce.user-orders.cancel", + "name": "주문 취소" + }, + { + "id": 85, + "identifier": "sirsoft-ecommerce.user-orders.confirm", + "name": "구매확정" + }, + { + "id": 87, + "identifier": "sirsoft-ecommerce.user-reviews.write", + "name": "리뷰 작성" + }, + { + "id": 23, + "identifier": "sirsoft-ecommerce.products.read", + "name": "상품 조회" + }, + { + "id": 24, + "identifier": "sirsoft-ecommerce.products.create", + "name": "상품 생성" + }, + { + "id": 25, + "identifier": "sirsoft-ecommerce.products.update", + "name": "상품 수정" + }, + { + "id": 26, + "identifier": "sirsoft-ecommerce.products.delete", + "name": "상품 삭제" + }, + { + "id": 28, + "identifier": "sirsoft-ecommerce.orders.read", + "name": "주문 조회" + }, + { + "id": 29, + "identifier": "sirsoft-ecommerce.orders.update", + "name": "주문 수정" + }, + { + "id": 31, + "identifier": "sirsoft-ecommerce.categories.read", + "name": "카테고리 조회" + }, + { + "id": 32, + "identifier": "sirsoft-ecommerce.categories.create", + "name": "카테고리 생성" + }, + { + "id": 33, + "identifier": "sirsoft-ecommerce.categories.update", + "name": "카테고리 수정" + }, + { + "id": 34, + "identifier": "sirsoft-ecommerce.categories.delete", + "name": "카테고리 삭제" + }, + { + "id": 36, + "identifier": "sirsoft-ecommerce.brands.read", + "name": "브랜드 조회" + }, + { + "id": 37, + "identifier": "sirsoft-ecommerce.brands.create", + "name": "브랜드 생성" + }, + { + "id": 38, + "identifier": "sirsoft-ecommerce.brands.update", + "name": "브랜드 수정" + }, + { + "id": 39, + "identifier": "sirsoft-ecommerce.brands.delete", + "name": "브랜드 삭제" + }, + { + "id": 41, + "identifier": "sirsoft-ecommerce.product-notice-templates.read", + "name": "조회" + }, + { + "id": 42, + "identifier": "sirsoft-ecommerce.product-notice-templates.create", + "name": "생성" + }, + { + "id": 43, + "identifier": "sirsoft-ecommerce.product-notice-templates.update", + "name": "수정" + }, + { + "id": 44, + "identifier": "sirsoft-ecommerce.product-notice-templates.delete", + "name": "삭제" + }, + { + "id": 46, + "identifier": "sirsoft-ecommerce.product-common-infos.read", + "name": "조회" + }, + { + "id": 47, + "identifier": "sirsoft-ecommerce.product-common-infos.create", + "name": "생성" + }, + { + "id": 48, + "identifier": "sirsoft-ecommerce.product-common-infos.update", + "name": "수정" + }, + { + "id": 49, + "identifier": "sirsoft-ecommerce.product-common-infos.delete", + "name": "삭제" + }, + { + "id": 51, + "identifier": "sirsoft-ecommerce.settings.read", + "name": "환경설정 조회" + }, + { + "id": 52, + "identifier": "sirsoft-ecommerce.settings.update", + "name": "환경설정 수정" + }, + { + "id": 54, + "identifier": "sirsoft-ecommerce.promotion-coupon.read", + "name": "쿠폰 조회" + }, + { + "id": 55, + "identifier": "sirsoft-ecommerce.promotion-coupon.create", + "name": "쿠폰 생성" + }, + { + "id": 56, + "identifier": "sirsoft-ecommerce.promotion-coupon.update", + "name": "쿠폰 수정" + }, + { + "id": 57, + "identifier": "sirsoft-ecommerce.promotion-coupon.delete", + "name": "쿠폰 삭제" + }, + { + "id": 59, + "identifier": "sirsoft-ecommerce.shipping-policies.read", + "name": "배송정책 조회" + }, + { + "id": 60, + "identifier": "sirsoft-ecommerce.shipping-policies.create", + "name": "배송정책 생성" + }, + { + "id": 61, + "identifier": "sirsoft-ecommerce.shipping-policies.update", + "name": "배송정책 수정" + }, + { + "id": 62, + "identifier": "sirsoft-ecommerce.shipping-policies.delete", + "name": "배송정책 삭제" + }, + { + "id": 64, + "identifier": "sirsoft-ecommerce.product-labels.read", + "name": "상품 라벨 조회" + }, + { + "id": 65, + "identifier": "sirsoft-ecommerce.product-labels.create", + "name": "상품 라벨 생성" + }, + { + "id": 66, + "identifier": "sirsoft-ecommerce.product-labels.update", + "name": "상품 라벨 수정" + }, + { + "id": 67, + "identifier": "sirsoft-ecommerce.product-labels.delete", + "name": "상품 라벨 삭제" + }, + { + "id": 69, + "identifier": "sirsoft-ecommerce.identity.policies.read", + "name": "본인인증 정책 조회" + }, + { + "id": 70, + "identifier": "sirsoft-ecommerce.identity.policies.update", + "name": "본인인증 정책 수정" + }, + { + "id": 72, + "identifier": "sirsoft-ecommerce.reviews.read", + "name": "리뷰 조회" + }, + { + "id": 73, + "identifier": "sirsoft-ecommerce.reviews.update", + "name": "리뷰 처리" + }, + { + "id": 74, + "identifier": "sirsoft-ecommerce.reviews.delete", + "name": "리뷰 삭제" + }, + { + "id": 76, + "identifier": "sirsoft-ecommerce.inquiries.update", + "name": "문의 처리" + }, + { + "id": 77, + "identifier": "sirsoft-ecommerce.inquiries.delete", + "name": "문의 삭제" + }, + { + "id": 79, + "identifier": "sirsoft-ecommerce.dashboard.view", + "name": "대시보드 조회" + }, + { + "id": 89, + "identifier": "sirsoft-ecommerce.mileage.read", + "name": "마일리지 내역 조회" + }, + { + "id": 90, + "identifier": "sirsoft-ecommerce.mileage.manage", + "name": "마일리지 수동 처리" + }, + { + "id": 92, + "identifier": "sirsoft-ecommerce.user-currency.manage", + "name": "회원 결제 통화 변경" + }, + { + "id": 94, + "identifier": "sirsoft-ecommerce.user-shipping-country.manage", + "name": "회원 배송국가 변경" + }, + { + "id": 96, + "identifier": "sirsoft-board.boards.read", + "name": "게시판 조회" + }, + { + "id": 97, + "identifier": "sirsoft-board.boards.create", + "name": "게시판 생성" + }, + { + "id": 98, + "identifier": "sirsoft-board.boards.update", + "name": "게시판 수정" + }, + { + "id": 99, + "identifier": "sirsoft-board.boards.delete", + "name": "게시판 삭제" + }, + { + "id": 101, + "identifier": "sirsoft-board.settings.read", + "name": "환경설정 조회" + }, + { + "id": 102, + "identifier": "sirsoft-board.settings.update", + "name": "환경설정 수정" + }, + { + "id": 104, + "identifier": "sirsoft-board.identity.policies.read", + "name": "본인인증 정책 조회" + }, + { + "id": 105, + "identifier": "sirsoft-board.identity.policies.update", + "name": "본인인증 정책 수정" + }, + { + "id": 107, + "identifier": "sirsoft-board.reports.view", + "name": "신고 조회" + }, + { + "id": 108, + "identifier": "sirsoft-board.reports.manage", + "name": "신고 처리" + }, + { + "id": 111, + "identifier": "sirsoft-page.pages.read", + "name": "페이지 조회" + }, + { + "id": 112, + "identifier": "sirsoft-page.pages.create", + "name": "페이지 생성" + }, + { + "id": 113, + "identifier": "sirsoft-page.pages.update", + "name": "페이지 수정" + }, + { + "id": 114, + "identifier": "sirsoft-page.pages.delete", + "name": "페이지 삭제" + }, + { + "id": 117, + "identifier": "core.users.read", + "name": "사용자 조회" + }, + { + "id": 118, + "identifier": "core.users.create", + "name": "사용자 생성" + }, + { + "id": 119, + "identifier": "core.users.update", + "name": "사용자 수정" + }, + { + "id": 120, + "identifier": "core.users.delete", + "name": "사용자 삭제" + }, + { + "id": 122, + "identifier": "core.menus.read", + "name": "메뉴 조회" + }, + { + "id": 123, + "identifier": "core.menus.create", + "name": "메뉴 생성" + }, + { + "id": 124, + "identifier": "core.menus.update", + "name": "메뉴 수정" + }, + { + "id": 125, + "identifier": "core.menus.delete", + "name": "메뉴 삭제" + }, + { + "id": 127, + "identifier": "core.modules.read", + "name": "모듈 조회" + }, + { + "id": 128, + "identifier": "core.modules.install", + "name": "모듈 설치" + }, + { + "id": 129, + "identifier": "core.modules.activate", + "name": "모듈 활성화" + }, + { + "id": 130, + "identifier": "core.modules.uninstall", + "name": "모듈 삭제" + }, + { + "id": 132, + "identifier": "core.plugins.read", + "name": "플러그인 조회" + }, + { + "id": 133, + "identifier": "core.plugins.install", + "name": "플러그인 설치" + }, + { + "id": 134, + "identifier": "core.plugins.activate", + "name": "플러그인 활성화" + }, + { + "id": 135, + "identifier": "core.plugins.update", + "name": "플러그인 설정" + }, + { + "id": 136, + "identifier": "core.plugins.uninstall", + "name": "플러그인 삭제" + }, + { + "id": 138, + "identifier": "core.templates.read", + "name": "템플릿 조회" + }, + { + "id": 139, + "identifier": "core.templates.install", + "name": "템플릿 설치" + }, + { + "id": 140, + "identifier": "core.templates.activate", + "name": "템플릿 활성화" + }, + { + "id": 141, + "identifier": "core.templates.uninstall", + "name": "템플릿 삭제" + }, + { + "id": 142, + "identifier": "core.templates.layouts.edit", + "name": "레이아웃 편집" + }, + { + "id": 144, + "identifier": "core.permissions.read", + "name": "권한 조회" + }, + { + "id": 145, + "identifier": "core.permissions.create", + "name": "역할 생성" + }, + { + "id": 146, + "identifier": "core.permissions.update", + "name": "역할 수정" + }, + { + "id": 147, + "identifier": "core.permissions.delete", + "name": "역할 삭제" + }, + { + "id": 149, + "identifier": "core.notification-logs.read", + "name": "발송 이력 조회" + }, + { + "id": 150, + "identifier": "core.notification-logs.delete", + "name": "발송 이력 삭제" + }, + { + "id": 152, + "identifier": "core.notifications.read", + "name": "알림 조회" + }, + { + "id": 153, + "identifier": "core.notifications.update", + "name": "알림 읽음 처리" + }, + { + "id": 154, + "identifier": "core.notifications.delete", + "name": "알림 삭제" + }, + { + "id": 156, + "identifier": "core.user-notifications.read", + "name": "알림 조회" + }, + { + "id": 157, + "identifier": "core.user-notifications.update", + "name": "알림 읽음 처리" + }, + { + "id": 158, + "identifier": "core.user-notifications.delete", + "name": "알림 삭제" + }, + { + "id": 160, + "identifier": "core.identity.request", + "name": "IDV 요청" + }, + { + "id": 161, + "identifier": "core.identity.verify", + "name": "IDV 검증" + }, + { + "id": 162, + "identifier": "core.identity.cancel", + "name": "IDV 취소" + }, + { + "id": 164, + "identifier": "core.admin.identity.providers.read", + "name": "프로바이더 설정 조회" + }, + { + "id": 165, + "identifier": "core.admin.identity.providers.update", + "name": "프로바이더 설정 수정" + }, + { + "id": 166, + "identifier": "core.admin.identity.policies.read", + "name": "정책 조회" + }, + { + "id": 167, + "identifier": "core.admin.identity.policies.update", + "name": "정책 수정" + }, + { + "id": 168, + "identifier": "core.admin.identity.logs.read", + "name": "로그 열람" + }, + { + "id": 169, + "identifier": "core.admin.identity.logs.purge", + "name": "로그 파기" + }, + { + "id": 170, + "identifier": "core.admin.identity.messages.read", + "name": "메시지 템플릿 조회" + }, + { + "id": 171, + "identifier": "core.admin.identity.messages.update", + "name": "메시지 템플릿 수정" + }, + { + "id": 173, + "identifier": "core.settings.read", + "name": "설정 조회" + }, + { + "id": 174, + "identifier": "core.settings.update", + "name": "설정 수정" + }, + { + "id": 176, + "identifier": "core.dashboard.read", + "name": "대시보드 조회" + }, + { + "id": 177, + "identifier": "core.dashboard.system-status", + "name": "시스템 상태" + }, + { + "id": 178, + "identifier": "core.dashboard.resources", + "name": "시스템 리소스" + }, + { + "id": 179, + "identifier": "core.dashboard.activities", + "name": "최근 활동" + }, + { + "id": 180, + "identifier": "core.dashboard.alerts", + "name": "시스템 알림" + }, + { + "id": 182, + "identifier": "core.activities.read", + "name": "활동 로그 조회" + }, + { + "id": 183, + "identifier": "core.activities.delete", + "name": "활동 로그 삭제" + }, + { + "id": 185, + "identifier": "core.attachments.create", + "name": "첨부파일 업로드" + }, + { + "id": 186, + "identifier": "core.attachments.update", + "name": "첨부파일 수정" + }, + { + "id": 187, + "identifier": "core.attachments.delete", + "name": "첨부파일 삭제" + }, + { + "id": 189, + "identifier": "core.schedules.read", + "name": "스케줄 조회" + }, + { + "id": 190, + "identifier": "core.schedules.create", + "name": "스케줄 생성" + }, + { + "id": 191, + "identifier": "core.schedules.update", + "name": "스케줄 수정" + }, + { + "id": 192, + "identifier": "core.schedules.delete", + "name": "스케줄 삭제" + }, + { + "id": 193, + "identifier": "core.schedules.run", + "name": "스케줄 실행" + }, + { + "id": 195, + "identifier": "core.language_packs.read", + "name": "언어팩 조회" + }, + { + "id": 196, + "identifier": "core.language_packs.install", + "name": "언어팩 설치" + }, + { + "id": 197, + "identifier": "core.language_packs.manage", + "name": "언어팩 관리" + }, + { + "id": 198, + "identifier": "core.language_packs.update", + "name": "언어팩 업데이트" + }, + { + "id": 1, + "identifier": "apidoc-sample.parent", + "name": "API 문서 샘플 권한" + }, + { + "id": 2, + "identifier": "apidoc-sample.child", + "name": "하위 권한" + }, + { + "id": 3, + "identifier": "sirsoft-board", + "name": "게시판" + }, + { + "id": 21, + "identifier": "sirsoft-ecommerce", + "name": "이커머스" + }, + { + "id": 109, + "identifier": "sirsoft-page", + "name": "페이지" + }, + { + "id": 4, + "identifier": "sirsoft-board.apidoc-sample-board", + "name": "API 문서 샘플 게시판 게시판" + }, + { + "id": 95, + "identifier": "sirsoft-board.boards", + "name": "게시판 관리" + }, + { + "id": 100, + "identifier": "sirsoft-board.settings", + "name": "환경설정" + }, + { + "id": 103, + "identifier": "sirsoft-board.identity.policies", + "name": "게시판 본인인증 정책" + }, + { + "id": 106, + "identifier": "sirsoft-board.reports", + "name": "게시판 신고 관리" + }, + { + "id": 22, + "identifier": "sirsoft-ecommerce.products", + "name": "상품 관리" + }, + { + "id": 27, + "identifier": "sirsoft-ecommerce.orders", + "name": "주문 관리" + }, + { + "id": 30, + "identifier": "sirsoft-ecommerce.categories", + "name": "카테고리 관리" + }, + { + "id": 35, + "identifier": "sirsoft-ecommerce.brands", + "name": "브랜드 관리" + }, + { + "id": 40, + "identifier": "sirsoft-ecommerce.product-notice-templates", + "name": "상품정보제공고시 관리" + }, + { + "id": 45, + "identifier": "sirsoft-ecommerce.product-common-infos", + "name": "공통정보 관리" + }, + { + "id": 50, + "identifier": "sirsoft-ecommerce.settings", + "name": "환경설정" + }, + { + "id": 53, + "identifier": "sirsoft-ecommerce.promotion-coupon", + "name": "쿠폰 관리" + }, + { + "id": 58, + "identifier": "sirsoft-ecommerce.shipping-policies", + "name": "배송정책 관리" + }, + { + "id": 63, + "identifier": "sirsoft-ecommerce.product-labels", + "name": "상품 라벨 관리" + }, + { + "id": 68, + "identifier": "sirsoft-ecommerce.identity.policies", + "name": "이커머스 본인인증 정책" + }, + { + "id": 71, + "identifier": "sirsoft-ecommerce.reviews", + "name": "리뷰 관리" + }, + { + "id": 75, + "identifier": "sirsoft-ecommerce.inquiries", + "name": "문의 관리" + }, + { + "id": 78, + "identifier": "sirsoft-ecommerce.dashboard", + "name": "대시보드" + }, + { + "id": 80, + "identifier": "sirsoft-ecommerce.user-products", + "name": "사용자 상품" + }, + { + "id": 82, + "identifier": "sirsoft-ecommerce.user-orders", + "name": "사용자 주문" + }, + { + "id": 86, + "identifier": "sirsoft-ecommerce.user-reviews", + "name": "사용자 리뷰" + }, + { + "id": 88, + "identifier": "sirsoft-ecommerce.mileage", + "name": "마일리지 관리" + }, + { + "id": 91, + "identifier": "sirsoft-ecommerce.user-currency", + "name": "회원 결제 통화 관리" + }, + { + "id": 93, + "identifier": "sirsoft-ecommerce.user-shipping-country", + "name": "회원 배송국가 관리" + }, + { + "id": 110, + "identifier": "sirsoft-page.pages", + "name": "페이지 관리" + }, + { + "id": 115, + "identifier": "core", + "name": "코어" + }, + { + "id": 116, + "identifier": "core.users", + "name": "사용자 관리" + }, + { + "id": 121, + "identifier": "core.menus", + "name": "메뉴 관리" + }, + { + "id": 126, + "identifier": "core.modules", + "name": "모듈 관리" + }, + { + "id": 131, + "identifier": "core.plugins", + "name": "플러그인 관리" + }, + { + "id": 137, + "identifier": "core.templates", + "name": "템플릿 관리" + }, + { + "id": 143, + "identifier": "core.permissions", + "name": "권한 관리" + }, + { + "id": 148, + "identifier": "core.notification-logs", + "name": "알림 발송 이력" + }, + { + "id": 151, + "identifier": "core.notifications", + "name": "알림 (관리자)" + }, + { + "id": 155, + "identifier": "core.user-notifications", + "name": "알림 (사용자)" + }, + { + "id": 159, + "identifier": "core.identity", + "name": "본인인증 (사용자)" + }, + { + "id": 163, + "identifier": "core.admin.identity", + "name": "본인인증 관리 (관리자)" + }, + { + "id": 172, + "identifier": "core.settings", + "name": "환경설정" + }, + { + "id": 175, + "identifier": "core.dashboard", + "name": "대시보드" + }, + { + "id": 181, + "identifier": "core.activities", + "name": "활동 로그" + }, + { + "id": 184, + "identifier": "core.attachments", + "name": "첨부파일 관리" + }, + { + "id": 188, + "identifier": "core.schedules", + "name": "스케줄 관리" + }, + { + "id": 194, + "identifier": "core.language_packs", + "name": "언어팩 관리" + } + ], + "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 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -147,10 +1300,28 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.login_validation_rules`). +**요청 예시** + +```http +POST /api/auth/admin/login HTTP/1.1 +Host: api.example.com +Accept: application/json +Content-Type: application/json + +{ + "email": "user@example.com", + "password": "Password123!" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -179,10 +1350,28 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.forgot_password_validation_rules`). +**요청 예시** + +```http +POST /api/auth/forgot-password HTTP/1.1 +Host: api.example.com +Accept: application/json +Content-Type: application/json + +{ + "email": "user@example.com", + "redirect_prefix": "admin" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -211,10 +1400,28 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.login_validation_rules`). +**요청 예시** + +```http +POST /api/auth/login HTTP/1.1 +Host: api.example.com +Accept: application/json +Content-Type: application/json + +{ + "email": "user@example.com", + "password": "Password123!" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -238,9 +1445,34 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/auth/logout HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "로그아웃이 성공했습니다.", + "data": null +} +``` **에러 응답** @@ -281,10 +1513,35 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.register_validation_rules`). +**요청 예시** + +```http +POST /api/auth/register HTTP/1.1 +Host: api.example.com +Accept: application/json +Content-Type: application/json + +{ + "name": "예시 이름", + "nickname": "예시 이름", + "email": "user@example.com", + "password": "Password123!", + "language": "ko", + "agree_terms": "예시값", + "agree_privacy": "예시값", + "preferred_currency": "예시값", + "preferred_shipping_country": "KR" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -314,10 +1571,29 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.reset_password_validation_rules`). +**요청 예시** + +```http +POST /api/auth/reset-password HTTP/1.1 +Host: api.example.com +Accept: application/json +Content-Type: application/json + +{ + "token": "{YOUR_TOKEN}", + "email": "user@example.com", + "password": "Password123!" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -341,6 +1617,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/auth/user HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -416,6 +1701,1077 @@ _단건 응답: `data` 객체의 필드._ | ecommerce_preferred_shipping_country | null | `null` | 선호 배송 국가 코드 (ecommerce 모듈 주입, 미설정 시 null) | | ecommerce_preferred_shipping_country_name | null | `null` | 선호 배송 국가 이름 (국가 코드에서 현지화 파생 — ecommerce 모듈 주입, 미설정 시 null) | +**응답 예시** + +```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", + "avatar": null, + "language": "ko", + "language_label": "한국어", + "country": "KR", + "status": "active", + "status_label": "활성", + "status_variant": "success", + "is_admin": true, + "homepage": "https://example.com", + "mobile": "010-9595-2897", + "phone": "02-637-5618", + "zipcode": "16505", + "address": "경기도 안양시 봉은사로 2918", + "address_detail": "48동 718호", + "signature": "Fugit consequuntur repellendus sed.", + "bio": "Ut magni et sunt ducimus error adipisci. Pariatur corporis voluptatem ratione quo non saepe. Illo atque praesentium possimus dolores qui est fugit. Sint fugiat numquam voluptates.", + "last_login_at": "2026-07-07 10:41:24", + "email_verified_at": "2026-07-08 10:41:24", + "timezone": "Asia/Seoul", + "modules_count": [], + "plugins_count": [], + "menus_count": [], + "modules": [], + "plugins": [], + "menus": [], + "roles": [ + { + "id": 1, + "identifier": "admin", + "name": "관리자" + }, + { + "id": 4, + "identifier": "apidoc-sample-role", + "name": "API 문서 샘플 역할" + } + ], + "permissions": [ + { + "id": 5, + "identifier": "sirsoft-board.apidoc-sample-board.admin.posts.read", + "name": "게시글 조회 (관리자)" + }, + { + "id": 6, + "identifier": "sirsoft-board.apidoc-sample-board.admin.posts.write", + "name": "게시글 작성/수정/삭제 (관리자)" + }, + { + "id": 7, + "identifier": "sirsoft-board.apidoc-sample-board.admin.posts.read-secret", + "name": "비밀글 조회 (관리자)" + }, + { + "id": 8, + "identifier": "sirsoft-board.apidoc-sample-board.admin.comments.read", + "name": "댓글 조회 (관리자)" + }, + { + "id": 9, + "identifier": "sirsoft-board.apidoc-sample-board.admin.comments.write", + "name": "댓글 작성/수정/삭제 (관리자)" + }, + { + "id": 10, + "identifier": "sirsoft-board.apidoc-sample-board.admin.attachments.upload", + "name": "파일 업로드/삭제 (관리자)" + }, + { + "id": 11, + "identifier": "sirsoft-board.apidoc-sample-board.admin.attachments.download", + "name": "파일 다운로드 (관리자)" + }, + { + "id": 12, + "identifier": "sirsoft-board.apidoc-sample-board.admin.manage", + "name": "게시판 관리 (관리자)" + }, + { + "id": 13, + "identifier": "sirsoft-board.apidoc-sample-board.posts.read", + "name": "게시글 조회" + }, + { + "id": 14, + "identifier": "sirsoft-board.apidoc-sample-board.posts.write", + "name": "게시글 작성/수정/삭제" + }, + { + "id": 15, + "identifier": "sirsoft-board.apidoc-sample-board.posts.read-secret", + "name": "비밀글 조회" + }, + { + "id": 16, + "identifier": "sirsoft-board.apidoc-sample-board.comments.read", + "name": "댓글 조회" + }, + { + "id": 17, + "identifier": "sirsoft-board.apidoc-sample-board.comments.write", + "name": "댓글 작성/수정/삭제" + }, + { + "id": 18, + "identifier": "sirsoft-board.apidoc-sample-board.attachments.upload", + "name": "파일 업로드/삭제" + }, + { + "id": 19, + "identifier": "sirsoft-board.apidoc-sample-board.attachments.download", + "name": "파일 다운로드" + }, + { + "id": 20, + "identifier": "sirsoft-board.apidoc-sample-board.manager", + "name": "게시판 관리 (사용자)" + }, + { + "id": 81, + "identifier": "sirsoft-ecommerce.user-products.read", + "name": "상품 조회" + }, + { + "id": 83, + "identifier": "sirsoft-ecommerce.user-orders.create", + "name": "주문하기" + }, + { + "id": 84, + "identifier": "sirsoft-ecommerce.user-orders.cancel", + "name": "주문 취소" + }, + { + "id": 85, + "identifier": "sirsoft-ecommerce.user-orders.confirm", + "name": "구매확정" + }, + { + "id": 87, + "identifier": "sirsoft-ecommerce.user-reviews.write", + "name": "리뷰 작성" + }, + { + "id": 23, + "identifier": "sirsoft-ecommerce.products.read", + "name": "상품 조회" + }, + { + "id": 24, + "identifier": "sirsoft-ecommerce.products.create", + "name": "상품 생성" + }, + { + "id": 25, + "identifier": "sirsoft-ecommerce.products.update", + "name": "상품 수정" + }, + { + "id": 26, + "identifier": "sirsoft-ecommerce.products.delete", + "name": "상품 삭제" + }, + { + "id": 28, + "identifier": "sirsoft-ecommerce.orders.read", + "name": "주문 조회" + }, + { + "id": 29, + "identifier": "sirsoft-ecommerce.orders.update", + "name": "주문 수정" + }, + { + "id": 31, + "identifier": "sirsoft-ecommerce.categories.read", + "name": "카테고리 조회" + }, + { + "id": 32, + "identifier": "sirsoft-ecommerce.categories.create", + "name": "카테고리 생성" + }, + { + "id": 33, + "identifier": "sirsoft-ecommerce.categories.update", + "name": "카테고리 수정" + }, + { + "id": 34, + "identifier": "sirsoft-ecommerce.categories.delete", + "name": "카테고리 삭제" + }, + { + "id": 36, + "identifier": "sirsoft-ecommerce.brands.read", + "name": "브랜드 조회" + }, + { + "id": 37, + "identifier": "sirsoft-ecommerce.brands.create", + "name": "브랜드 생성" + }, + { + "id": 38, + "identifier": "sirsoft-ecommerce.brands.update", + "name": "브랜드 수정" + }, + { + "id": 39, + "identifier": "sirsoft-ecommerce.brands.delete", + "name": "브랜드 삭제" + }, + { + "id": 41, + "identifier": "sirsoft-ecommerce.product-notice-templates.read", + "name": "조회" + }, + { + "id": 42, + "identifier": "sirsoft-ecommerce.product-notice-templates.create", + "name": "생성" + }, + { + "id": 43, + "identifier": "sirsoft-ecommerce.product-notice-templates.update", + "name": "수정" + }, + { + "id": 44, + "identifier": "sirsoft-ecommerce.product-notice-templates.delete", + "name": "삭제" + }, + { + "id": 46, + "identifier": "sirsoft-ecommerce.product-common-infos.read", + "name": "조회" + }, + { + "id": 47, + "identifier": "sirsoft-ecommerce.product-common-infos.create", + "name": "생성" + }, + { + "id": 48, + "identifier": "sirsoft-ecommerce.product-common-infos.update", + "name": "수정" + }, + { + "id": 49, + "identifier": "sirsoft-ecommerce.product-common-infos.delete", + "name": "삭제" + }, + { + "id": 51, + "identifier": "sirsoft-ecommerce.settings.read", + "name": "환경설정 조회" + }, + { + "id": 52, + "identifier": "sirsoft-ecommerce.settings.update", + "name": "환경설정 수정" + }, + { + "id": 54, + "identifier": "sirsoft-ecommerce.promotion-coupon.read", + "name": "쿠폰 조회" + }, + { + "id": 55, + "identifier": "sirsoft-ecommerce.promotion-coupon.create", + "name": "쿠폰 생성" + }, + { + "id": 56, + "identifier": "sirsoft-ecommerce.promotion-coupon.update", + "name": "쿠폰 수정" + }, + { + "id": 57, + "identifier": "sirsoft-ecommerce.promotion-coupon.delete", + "name": "쿠폰 삭제" + }, + { + "id": 59, + "identifier": "sirsoft-ecommerce.shipping-policies.read", + "name": "배송정책 조회" + }, + { + "id": 60, + "identifier": "sirsoft-ecommerce.shipping-policies.create", + "name": "배송정책 생성" + }, + { + "id": 61, + "identifier": "sirsoft-ecommerce.shipping-policies.update", + "name": "배송정책 수정" + }, + { + "id": 62, + "identifier": "sirsoft-ecommerce.shipping-policies.delete", + "name": "배송정책 삭제" + }, + { + "id": 64, + "identifier": "sirsoft-ecommerce.product-labels.read", + "name": "상품 라벨 조회" + }, + { + "id": 65, + "identifier": "sirsoft-ecommerce.product-labels.create", + "name": "상품 라벨 생성" + }, + { + "id": 66, + "identifier": "sirsoft-ecommerce.product-labels.update", + "name": "상품 라벨 수정" + }, + { + "id": 67, + "identifier": "sirsoft-ecommerce.product-labels.delete", + "name": "상품 라벨 삭제" + }, + { + "id": 69, + "identifier": "sirsoft-ecommerce.identity.policies.read", + "name": "본인인증 정책 조회" + }, + { + "id": 70, + "identifier": "sirsoft-ecommerce.identity.policies.update", + "name": "본인인증 정책 수정" + }, + { + "id": 72, + "identifier": "sirsoft-ecommerce.reviews.read", + "name": "리뷰 조회" + }, + { + "id": 73, + "identifier": "sirsoft-ecommerce.reviews.update", + "name": "리뷰 처리" + }, + { + "id": 74, + "identifier": "sirsoft-ecommerce.reviews.delete", + "name": "리뷰 삭제" + }, + { + "id": 76, + "identifier": "sirsoft-ecommerce.inquiries.update", + "name": "문의 처리" + }, + { + "id": 77, + "identifier": "sirsoft-ecommerce.inquiries.delete", + "name": "문의 삭제" + }, + { + "id": 79, + "identifier": "sirsoft-ecommerce.dashboard.view", + "name": "대시보드 조회" + }, + { + "id": 89, + "identifier": "sirsoft-ecommerce.mileage.read", + "name": "마일리지 내역 조회" + }, + { + "id": 90, + "identifier": "sirsoft-ecommerce.mileage.manage", + "name": "마일리지 수동 처리" + }, + { + "id": 92, + "identifier": "sirsoft-ecommerce.user-currency.manage", + "name": "회원 결제 통화 변경" + }, + { + "id": 94, + "identifier": "sirsoft-ecommerce.user-shipping-country.manage", + "name": "회원 배송국가 변경" + }, + { + "id": 96, + "identifier": "sirsoft-board.boards.read", + "name": "게시판 조회" + }, + { + "id": 97, + "identifier": "sirsoft-board.boards.create", + "name": "게시판 생성" + }, + { + "id": 98, + "identifier": "sirsoft-board.boards.update", + "name": "게시판 수정" + }, + { + "id": 99, + "identifier": "sirsoft-board.boards.delete", + "name": "게시판 삭제" + }, + { + "id": 101, + "identifier": "sirsoft-board.settings.read", + "name": "환경설정 조회" + }, + { + "id": 102, + "identifier": "sirsoft-board.settings.update", + "name": "환경설정 수정" + }, + { + "id": 104, + "identifier": "sirsoft-board.identity.policies.read", + "name": "본인인증 정책 조회" + }, + { + "id": 105, + "identifier": "sirsoft-board.identity.policies.update", + "name": "본인인증 정책 수정" + }, + { + "id": 107, + "identifier": "sirsoft-board.reports.view", + "name": "신고 조회" + }, + { + "id": 108, + "identifier": "sirsoft-board.reports.manage", + "name": "신고 처리" + }, + { + "id": 111, + "identifier": "sirsoft-page.pages.read", + "name": "페이지 조회" + }, + { + "id": 112, + "identifier": "sirsoft-page.pages.create", + "name": "페이지 생성" + }, + { + "id": 113, + "identifier": "sirsoft-page.pages.update", + "name": "페이지 수정" + }, + { + "id": 114, + "identifier": "sirsoft-page.pages.delete", + "name": "페이지 삭제" + }, + { + "id": 117, + "identifier": "core.users.read", + "name": "사용자 조회" + }, + { + "id": 118, + "identifier": "core.users.create", + "name": "사용자 생성" + }, + { + "id": 119, + "identifier": "core.users.update", + "name": "사용자 수정" + }, + { + "id": 120, + "identifier": "core.users.delete", + "name": "사용자 삭제" + }, + { + "id": 122, + "identifier": "core.menus.read", + "name": "메뉴 조회" + }, + { + "id": 123, + "identifier": "core.menus.create", + "name": "메뉴 생성" + }, + { + "id": 124, + "identifier": "core.menus.update", + "name": "메뉴 수정" + }, + { + "id": 125, + "identifier": "core.menus.delete", + "name": "메뉴 삭제" + }, + { + "id": 127, + "identifier": "core.modules.read", + "name": "모듈 조회" + }, + { + "id": 128, + "identifier": "core.modules.install", + "name": "모듈 설치" + }, + { + "id": 129, + "identifier": "core.modules.activate", + "name": "모듈 활성화" + }, + { + "id": 130, + "identifier": "core.modules.uninstall", + "name": "모듈 삭제" + }, + { + "id": 132, + "identifier": "core.plugins.read", + "name": "플러그인 조회" + }, + { + "id": 133, + "identifier": "core.plugins.install", + "name": "플러그인 설치" + }, + { + "id": 134, + "identifier": "core.plugins.activate", + "name": "플러그인 활성화" + }, + { + "id": 135, + "identifier": "core.plugins.update", + "name": "플러그인 설정" + }, + { + "id": 136, + "identifier": "core.plugins.uninstall", + "name": "플러그인 삭제" + }, + { + "id": 138, + "identifier": "core.templates.read", + "name": "템플릿 조회" + }, + { + "id": 139, + "identifier": "core.templates.install", + "name": "템플릿 설치" + }, + { + "id": 140, + "identifier": "core.templates.activate", + "name": "템플릿 활성화" + }, + { + "id": 141, + "identifier": "core.templates.uninstall", + "name": "템플릿 삭제" + }, + { + "id": 142, + "identifier": "core.templates.layouts.edit", + "name": "레이아웃 편집" + }, + { + "id": 144, + "identifier": "core.permissions.read", + "name": "권한 조회" + }, + { + "id": 145, + "identifier": "core.permissions.create", + "name": "역할 생성" + }, + { + "id": 146, + "identifier": "core.permissions.update", + "name": "역할 수정" + }, + { + "id": 147, + "identifier": "core.permissions.delete", + "name": "역할 삭제" + }, + { + "id": 149, + "identifier": "core.notification-logs.read", + "name": "발송 이력 조회" + }, + { + "id": 150, + "identifier": "core.notification-logs.delete", + "name": "발송 이력 삭제" + }, + { + "id": 152, + "identifier": "core.notifications.read", + "name": "알림 조회" + }, + { + "id": 153, + "identifier": "core.notifications.update", + "name": "알림 읽음 처리" + }, + { + "id": 154, + "identifier": "core.notifications.delete", + "name": "알림 삭제" + }, + { + "id": 156, + "identifier": "core.user-notifications.read", + "name": "알림 조회" + }, + { + "id": 157, + "identifier": "core.user-notifications.update", + "name": "알림 읽음 처리" + }, + { + "id": 158, + "identifier": "core.user-notifications.delete", + "name": "알림 삭제" + }, + { + "id": 160, + "identifier": "core.identity.request", + "name": "IDV 요청" + }, + { + "id": 161, + "identifier": "core.identity.verify", + "name": "IDV 검증" + }, + { + "id": 162, + "identifier": "core.identity.cancel", + "name": "IDV 취소" + }, + { + "id": 164, + "identifier": "core.admin.identity.providers.read", + "name": "프로바이더 설정 조회" + }, + { + "id": 165, + "identifier": "core.admin.identity.providers.update", + "name": "프로바이더 설정 수정" + }, + { + "id": 166, + "identifier": "core.admin.identity.policies.read", + "name": "정책 조회" + }, + { + "id": 167, + "identifier": "core.admin.identity.policies.update", + "name": "정책 수정" + }, + { + "id": 168, + "identifier": "core.admin.identity.logs.read", + "name": "로그 열람" + }, + { + "id": 169, + "identifier": "core.admin.identity.logs.purge", + "name": "로그 파기" + }, + { + "id": 170, + "identifier": "core.admin.identity.messages.read", + "name": "메시지 템플릿 조회" + }, + { + "id": 171, + "identifier": "core.admin.identity.messages.update", + "name": "메시지 템플릿 수정" + }, + { + "id": 173, + "identifier": "core.settings.read", + "name": "설정 조회" + }, + { + "id": 174, + "identifier": "core.settings.update", + "name": "설정 수정" + }, + { + "id": 176, + "identifier": "core.dashboard.read", + "name": "대시보드 조회" + }, + { + "id": 177, + "identifier": "core.dashboard.system-status", + "name": "시스템 상태" + }, + { + "id": 178, + "identifier": "core.dashboard.resources", + "name": "시스템 리소스" + }, + { + "id": 179, + "identifier": "core.dashboard.activities", + "name": "최근 활동" + }, + { + "id": 180, + "identifier": "core.dashboard.alerts", + "name": "시스템 알림" + }, + { + "id": 182, + "identifier": "core.activities.read", + "name": "활동 로그 조회" + }, + { + "id": 183, + "identifier": "core.activities.delete", + "name": "활동 로그 삭제" + }, + { + "id": 185, + "identifier": "core.attachments.create", + "name": "첨부파일 업로드" + }, + { + "id": 186, + "identifier": "core.attachments.update", + "name": "첨부파일 수정" + }, + { + "id": 187, + "identifier": "core.attachments.delete", + "name": "첨부파일 삭제" + }, + { + "id": 189, + "identifier": "core.schedules.read", + "name": "스케줄 조회" + }, + { + "id": 190, + "identifier": "core.schedules.create", + "name": "스케줄 생성" + }, + { + "id": 191, + "identifier": "core.schedules.update", + "name": "스케줄 수정" + }, + { + "id": 192, + "identifier": "core.schedules.delete", + "name": "스케줄 삭제" + }, + { + "id": 193, + "identifier": "core.schedules.run", + "name": "스케줄 실행" + }, + { + "id": 195, + "identifier": "core.language_packs.read", + "name": "언어팩 조회" + }, + { + "id": 196, + "identifier": "core.language_packs.install", + "name": "언어팩 설치" + }, + { + "id": 197, + "identifier": "core.language_packs.manage", + "name": "언어팩 관리" + }, + { + "id": 198, + "identifier": "core.language_packs.update", + "name": "언어팩 업데이트" + }, + { + "id": 1, + "identifier": "apidoc-sample.parent", + "name": "API 문서 샘플 권한" + }, + { + "id": 2, + "identifier": "apidoc-sample.child", + "name": "하위 권한" + }, + { + "id": 3, + "identifier": "sirsoft-board", + "name": "게시판" + }, + { + "id": 21, + "identifier": "sirsoft-ecommerce", + "name": "이커머스" + }, + { + "id": 109, + "identifier": "sirsoft-page", + "name": "페이지" + }, + { + "id": 4, + "identifier": "sirsoft-board.apidoc-sample-board", + "name": "API 문서 샘플 게시판 게시판" + }, + { + "id": 95, + "identifier": "sirsoft-board.boards", + "name": "게시판 관리" + }, + { + "id": 100, + "identifier": "sirsoft-board.settings", + "name": "환경설정" + }, + { + "id": 103, + "identifier": "sirsoft-board.identity.policies", + "name": "게시판 본인인증 정책" + }, + { + "id": 106, + "identifier": "sirsoft-board.reports", + "name": "게시판 신고 관리" + }, + { + "id": 22, + "identifier": "sirsoft-ecommerce.products", + "name": "상품 관리" + }, + { + "id": 27, + "identifier": "sirsoft-ecommerce.orders", + "name": "주문 관리" + }, + { + "id": 30, + "identifier": "sirsoft-ecommerce.categories", + "name": "카테고리 관리" + }, + { + "id": 35, + "identifier": "sirsoft-ecommerce.brands", + "name": "브랜드 관리" + }, + { + "id": 40, + "identifier": "sirsoft-ecommerce.product-notice-templates", + "name": "상품정보제공고시 관리" + }, + { + "id": 45, + "identifier": "sirsoft-ecommerce.product-common-infos", + "name": "공통정보 관리" + }, + { + "id": 50, + "identifier": "sirsoft-ecommerce.settings", + "name": "환경설정" + }, + { + "id": 53, + "identifier": "sirsoft-ecommerce.promotion-coupon", + "name": "쿠폰 관리" + }, + { + "id": 58, + "identifier": "sirsoft-ecommerce.shipping-policies", + "name": "배송정책 관리" + }, + { + "id": 63, + "identifier": "sirsoft-ecommerce.product-labels", + "name": "상품 라벨 관리" + }, + { + "id": 68, + "identifier": "sirsoft-ecommerce.identity.policies", + "name": "이커머스 본인인증 정책" + }, + { + "id": 71, + "identifier": "sirsoft-ecommerce.reviews", + "name": "리뷰 관리" + }, + { + "id": 75, + "identifier": "sirsoft-ecommerce.inquiries", + "name": "문의 관리" + }, + { + "id": 78, + "identifier": "sirsoft-ecommerce.dashboard", + "name": "대시보드" + }, + { + "id": 80, + "identifier": "sirsoft-ecommerce.user-products", + "name": "사용자 상품" + }, + { + "id": 82, + "identifier": "sirsoft-ecommerce.user-orders", + "name": "사용자 주문" + }, + { + "id": 86, + "identifier": "sirsoft-ecommerce.user-reviews", + "name": "사용자 리뷰" + }, + { + "id": 88, + "identifier": "sirsoft-ecommerce.mileage", + "name": "마일리지 관리" + }, + { + "id": 91, + "identifier": "sirsoft-ecommerce.user-currency", + "name": "회원 결제 통화 관리" + }, + { + "id": 93, + "identifier": "sirsoft-ecommerce.user-shipping-country", + "name": "회원 배송국가 관리" + }, + { + "id": 110, + "identifier": "sirsoft-page.pages", + "name": "페이지 관리" + }, + { + "id": 115, + "identifier": "core", + "name": "코어" + }, + { + "id": 116, + "identifier": "core.users", + "name": "사용자 관리" + }, + { + "id": 121, + "identifier": "core.menus", + "name": "메뉴 관리" + }, + { + "id": 126, + "identifier": "core.modules", + "name": "모듈 관리" + }, + { + "id": 131, + "identifier": "core.plugins", + "name": "플러그인 관리" + }, + { + "id": 137, + "identifier": "core.templates", + "name": "템플릿 관리" + }, + { + "id": 143, + "identifier": "core.permissions", + "name": "권한 관리" + }, + { + "id": 148, + "identifier": "core.notification-logs", + "name": "알림 발송 이력" + }, + { + "id": 151, + "identifier": "core.notifications", + "name": "알림 (관리자)" + }, + { + "id": 155, + "identifier": "core.user-notifications", + "name": "알림 (사용자)" + }, + { + "id": 159, + "identifier": "core.identity", + "name": "본인인증 (사용자)" + }, + { + "id": 163, + "identifier": "core.admin.identity", + "name": "본인인증 관리 (관리자)" + }, + { + "id": 172, + "identifier": "core.settings", + "name": "환경설정" + }, + { + "id": 175, + "identifier": "core.dashboard", + "name": "대시보드" + }, + { + "id": 181, + "identifier": "core.activities", + "name": "활동 로그" + }, + { + "id": 184, + "identifier": "core.attachments", + "name": "첨부파일 관리" + }, + { + "id": 188, + "identifier": "core.schedules", + "name": "스케줄 관리" + }, + { + "id": 194, + "identifier": "core.language_packs", + "name": "언어팩 관리" + } + ], + "consents": [], + "terms_consent": [], + "privacy_consent": [], + "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 + }, + "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 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -446,10 +2802,28 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.validate_reset_token_rules`, `core.auth.validate_reset_token_messages`). +**요청 예시** + +```http +POST /api/auth/validate-reset-token HTTP/1.1 +Host: api.example.com +Accept: application/json +Content-Type: application/json + +{ + "token": "{YOUR_TOKEN}", + "email": "user@example.com" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -473,10 +2847,23 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/user/auth/logout HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -500,10 +2887,23 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/user/auth/logout-all-devices HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -527,10 +2927,23 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/user/auth/refresh HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -554,10 +2967,23 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/user/auth/user HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/avatar.md b/docs/backend/api/avatar.md index e36c90e0..fa1fe9cf 100644 --- a/docs/backend/api/avatar.md +++ b/docs/backend/api/avatar.md @@ -27,10 +27,23 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +DELETE /api/me/avatar HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -56,10 +69,31 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.upload_avatar_rules`). +**요청 예시** + +```http +POST /api/me/avatar HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="avatar"; filename="example.png" +Content-Type: image/png + +(바이너리 파일 내용) +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/broadcasting.md b/docs/backend/api/broadcasting.md index 81ef8951..a2ee94ce 100644 --- a/docs/backend/api/broadcasting.md +++ b/docs/backend/api/broadcasting.md @@ -26,10 +26,23 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/broadcasting/auth HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/changelog.md b/docs/backend/api/changelog.md index f078e7fa..d903423a 100644 --- a/docs/backend/api/changelog.md +++ b/docs/backend/api/changelog.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/changelog HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -35,6 +44,22 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | content | string | `# Changelog 이 프로젝트의 모든 주요 변경사항을 기록합니…` | 본문 내용 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "성공적으로 처리되었습니다.", + "data": { + "content": "# Changelog\n\n이 프로젝트의 모든 주요 변경사항을 기록합니다.\n형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,\n[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.\n\n## [7.0.2] - 2026-07-05\n\n### Added\n\n- 설치된 모듈·플러그인이 늘어날수록 화면마다 개별로 내려받던 확장 스크립트·스타일 요청을 종류별로 하나로 묶어 내려받도록 개선해, 확장이 많은 사이트의 초기 로딩 요청 수를 크게 줄였습니다. 확장을 설치·활성화·비활성화하면 묶음이 자동으로 다시 만들어져 항상 현재 활성 상태와 일치합니다.\n\n### Changed\n\n- 화면·데이터를 내려받을 때의 전송 용량을 줄여 초기 로딩과 페이지 이동 속도를 개선했습니다.\n - 모듈·플러그인의 묶음 스크립트·스타일이 지금까지 압축 없이 전송되던 것을 압축 전송하도록 바꿔, 확장이 많은 사이트에서 첫 화면에 받는 용량을 크게 줄였습니다.\n - 한글 등 다국어 텍스트가 응답에 불필요하게 부풀려 담기던 것을 원래 형태 그대로 담도록 바꿔, 특히 다국어 번역 데이터의 전송 용량을 줄였습니다.\n - 화면을 그리는 데 쓰이지 않는 개발용 설명 주석을 화면 정보 응답에서 빼도록 해 응답 용량을 추가로 줄였습니다.\n\n### Fixed\n\n- 코어 업데이트 시 레이아웃 편집기·개발자 진단도구 번들의 빌드 설정 파일이 갱신 대상에서 누락되던 문제를 수정했습니다. 이제 코어를 업데이트하면 이 파일들도 최신 내용으로 반영됩니다.\n- 설치 마법사에서 Vendor 설치 방식을 \"Composer 실행\"으로 선택해 설치할 때, 일부 Windows 환경에서 Composer 설치 로그의 문자 인코딩이 어긋나면 진행 상태 응답이 비어 진행 상황이 표시되지 않고 오류가 반복될 수 있던 문제를 보완했습니다. 어떤 문자가 섞이더라도 진행 로그가 항상 정상적으로 전달되도록 처리했습니다. (#62 @kitrio 님께서 제보해주셨습니다.)\n- 설치 마법사의 \"설치 환경 확인\" 단계에서, storage 폴더 하위에 파일이 많이 쌓인 환경일수록 요구사항 검사가 오래 걸리던 문제를 개선했습니다. 폴더 쓰기 권한 검사가 불필요하게 깊은 하위까지 훑던 것을 필요한 범위로 제한해 이 단계가 즉시 완료되도록 했습니다.\n- 설치 시 \"읽기 전용 데이터베이스(Read DB)를 사용하지 않음\"으로 선택했는데도, 읽기 전용 DB 접속 정보를 확인하려다 설치 마지막 단계에서 실패하던 문제를 수정했습니다. 이제 읽기 전용 DB를 사용하지 않으면 쓰기 DB 정보만으로 설치가 정상 완료됩니다. 아울러 설치 이후에도 읽기 전용 DB 접속 정보를 비워 두면 조회 연결이 자동으로 쓰기 DB로 대체되도록 하여, 별도의 읽기 전용 DB 없이도 안정적으로 동작합니다. (#63 @kitrio 님께서 제보해주셨습니다.)\n\n## [7.0.1] - 2026-07-03\n\n### Changed\n\n- 첫 화면 접속 시 내려받는 코어 스크립트 용량을 절반 이하로 줄여 초기 로딩을 개선했습니다. 레이아웃 편집기와 개발자 진단도구를 코어에서 분리해, 편집기는 편집 화면에 들어갈 때만, 진단도구는 디버그 모드를 켰을 때만 내려받도록 했습니다. 일반 사용자는 평소 쓰지 않는 편집기·진단도구 코드를 더 이상 받지 않습니다.\n- 설치가 끝난 사이트에서 매 요청마다 반복되던 불필요한 데이터베이스 조회를 줄여 페이지 응답 속도를 개선했습니다. 알림·본인인증 설정을 준비하는 과정에서 이미 존재가 보장된 테이블을 매번 다시 확인하던 동작을 설치 완료 상태에서는 건너뛰도록 했습니다.\n- 화면을 그릴 때마다 서버가 매번 확장 기능의 연결 정보를 처음부터 다시 계산하던 부담을 줄여 페이지 응답 속도를 개선했습니다. 이 연결 정보는 확장을 설치·수정하거나 코어를 업데이트할 때만 바뀌므로 그 시점에 한 번 저장해 두고, 이후 요청에서는 저장된 값을 재사용하도록 했습니다. 확장이나 코어를 변경하면 저장 값이 자동으로 갱신됩니다.\n- 설치된 모듈·플러그인의 코드를 찾을 때마다 서버가 파일을 하나하나 탐색하던 부담을 줄여, 특히 공유 호스팅 등 파일 접근이 느린 환경에서 페이지 응답 속도를 개선했습니다. 확장 코드의 위치를 설치·수정 시점에 미리 정리해 두고 이후 요청에서는 그 목록을 바로 참조하도록 했습니다. 확장을 설치·수정·삭제하거나 코어를 업데이트하면 목록이 자동으로 갱신됩니다.\n- 코어 업데이트 중 데이터 변환 단계가 현재 환경에서 자동으로 끝까지 실행되지 못하고 중단될 때, 남은 단계를 이어서 실행하는 안내를 실행 권한에 맞게 개선했습니다. 특히 관리자 권한(sudo)으로 업데이트한 경우, 남은 단계를 관리자 권한 그대로 실행하면 그 과정에서 만들어지는 파일이 관리자 소유로 남아 이후 웹사이트가 그 파일에 쓰지 못하는 문제가 생길 수 있습니다. 이제 이런 경우 웹 서버 실행 계정(예: www-data)으로 실행하도록 정확한 명령을 함께 안내하며, 웹 서버 계정을 자동으로 확인하지 못하면 어떤 계정으로 실행해야 하는지 확인 방법을 안내합니다.\n\n### Fixed\n\n- Windows 환경에서 코어 업데이트가 `public/storage` 링크를 복사하는 단계에서 \"디렉토리는 복사할 수 없습니다\" 오류로 중단되던 문제를 수정했습니다. Windows의 저장소 링크(정션)가 이제 링크 그대로 안전하게 보존되어 업데이트가 정상 완료됩니다.\n- 별도 데이터 변환 작업이 필요 없는 버전으로 코어를 업데이트할 때, \"실행할 업그레이드 단계가 없다\"는 정상 상황을 실패로 표시하고 중단하던 문제를 수정했습니다. 이제 변환 작업이 없는 업데이트는 매끄럽게 완료되며, 이어서 설치된 확장의 최신 번들 반영 단계까지 정상적으로 진행됩니다.\n- 일부 호스팅 환경에서 관리자 환경설정의 보안 화면에 들어갈 때 시스템 정보 조회가 전체 실패(오류 응답)하던 문제를 수정했습니다. 이제 CPU·메모리·디스크 등 특정 항목을 서버 정책상 읽을 수 없더라도 그 항목만 \"알 수 없음\"으로 표시하고 나머지 시스템 정보는 정상적으로 보여줍니다. 읽지 못한 항목은 로그로 원인을 확인할 수 있습니다. (#59 @glitter-gim 님께서 제보해주셨습니다.)\n\n## [7.0.0] - 2026-07-01\n\n### Added\n\n- 모듈·플러그인이 기존 화면의 초기화 동작에 자신의 초기화 단계를 덧붙일 수 있도록 레이아웃 확장 기능을 확대했습니다. 화면이 특정 모듈을 직접 알지 않아도, 모듈이 설치되어 있을 때만 그 모듈의 초기화(예: 표시 통화 복원)가 함께 실행됩니다.\n\n- 관리자 대시보드 통계에 설치된 템플릿 수(활성/전체)와 활성 언어팩 수(활성/전체)를 추가했습니다.\n- 관리자 대시보드에 최근 발송된 알림 이력을 보여주는 \"최근 알림\" 위젯용 조회 기능을 추가했습니다. 발송 채널·수신자·제목·상태(성공/실패/건너뜀)·발송 시각을 최신순으로 확인할 수 있습니다.\n- 본인인증 challenge 취소(cancel) 와 verification 토큰 소비(consume_token) 시점에 외부 본인인증 플러그인이 자기 데이터 정리 로직을 등록할 수 있는 훅 4건 추가.\n- 플러그인 개발용 표준 베이스 클래스를 추가하여, 모듈 개발과 동일한 방식으로 Repository · 캐시 · 스토리지 의존성을 선언만으로 자동 주입받을 수 있도록 개선. 플러그인이 코어와 같은 표준 위에서 작성되어 일관된 캐시·파일 격리 동작을 제공합니다.\n- 모듈·플러그인이 페이지에 주입하는 레이아웃 확장을 관리자 화면에서 직접 편집할 수 있도록 개선. 편집 내역은 버전으로 기록되어 이전 버전으로 복구할 수 있으며, 저장 전 미리보기를 제공합니다.\n\n#### 위지윅 레이아웃 편집기 (신규 도입)\n\n화면을 직접 보며 편집하는 위지윅 방식의 레이아웃 편집기를 이번 버전에서 새로 도입했습니다. 아래는 도입된 기능 영역별 상세입니다.\n\n- 좌측 트리 — 편집 화면 좌측 목록을 트리 구조로 개편하여 일반 레이아웃과 확장 주입 레이아웃을 구분 표시. 확장 주입 레이아웃은 해당 템플릿에 실제 적용되는 항목만 표시됩니다.\n- 편집 진입과 캔버스 — 템플릿 관리 화면에 코드 편집과 레이아웃 편집 진입을 분리하고, 운영 화면 좌측 하단에 편집 권한 보유자 전용 진입 버튼을 추가했습니다. 편집 캔버스는 실제 페이지와 동일한 렌더링 결과를 보여주며, 데스크탑·태블릿·모바일 폭을 즉시 전환해 반응형 결과를 확인할 수 있습니다. 데이터 소스가 정의된 화면은 샘플 데이터로 자동 채워져 실제 API 없이도 화면을 검토할 수 있고, 본문 영역에 사용된 위지윅 에디터 등 동적 자원도 캔버스 안에서 그대로 동작합니다. 좌측 트리에서 화면을 선택하면 주소창이 즉시 반영되어 새로고침·공유·뒤로가기 동작이 자연스럽고, 같은 주소로 직접 진입하면 해당 화면이 자동 선택됩니다. 편집 중 로그인이 풀리면 안내 후 자동으로 로그인 화면으로 이동하며 재로그인 시 편집 화면으로 복귀합니다. 권한 부족·서버 오류·네트워크 끊김 등은 상황별 안내 카드(아이콘·설명·다음 행동 버튼)로 표시합니다.\n- 요소 추가·재배치 — 컴포넌트를 클릭해 선택하면 외곽에 + 버튼이 나타나고, + 버튼을 누르면 화면 중앙에 모달 팔레트가 열려 좌측 카테고리 사이드바와 우측 그리드 카드(아이콘 + 라벨 + 유형 뱃지)에서 추가할 컴포넌트를 고를 수 있습니다. 부모 컨테이너에 들어갈 수 있는 컴포넌트만 자동 필터링되며, 검색으로 컴포넌트를 빠르게 찾을 수 있습니다. 추가한 컴포넌트는 템플릿이 정의한 기본 모양·기본 텍스트로 캔버스에 바로 보이며, 추가 직후 해당 위치로 화면이 부드럽게 스크롤됩니다. 컴포넌트 우상단 ⓘ 아이콘으로 속성 설정·복사·삭제 메뉴를 열고, 호버/선택 시 외곽선·8방향 리사이즈 핸들 자리·잠금/네비게이션 어포던스가 표시됩니다. 변경 사항은 Ctrl+Z / Ctrl+Shift+Z 또는 툴바 ↶/↷ 로 되돌리거나 다시 실행할 수 있습니다. 편집 모드에서 컴포넌트를 클릭해도 페이지 이동이 일어나지 않고 선택만 되며, 내부 라우트로 가리키는 요소는 \"→ 이 화면 편집\" 어포던스로 목적지 화면 편집 모드로 즉시 전환됩니다.\n- 저장·충돌 방지 — 낙관적 잠금으로 저장 결과는 성공·검증 실패·동시 저장 충돌·네트워크 오류 등 상황별 안내(성공 토스트 / 오류 배너 / 충돌 모달)로 즉시 표시됩니다. 같은 레이아웃을 다른 관리자가 먼저 저장했으면 충돌 안내 모달에서 최신 버전 불러오기를 선택할 수 있고, 신규 추가한 컴포넌트가 그 사이 비활성화된 모듈·플러그인 소속이면 저장이 자동 차단됩니다. 저장 직후에는 되돌리기 이력이 정리되어 저장 시점이 새 기준이 되며, 변경 사항이 있는 상태에서 다른 화면으로 이동하려 하면 저장하지 않은 내용이 있다는 안내가 먼저 표시됩니다. 로그인·회원가입 등 비로그인 전용 화면을 편집할 때는 로그인 상태 미리보기가 적용되지 않아 안내 토스트가 잘못 뜨지 않습니다.\n- 속성 편집 — 요소를 선택하면 정렬·글자 크기·굵기·색상·여백·크기 등을 친화적인 컨트롤(분절 버튼·슬라이더·색상 선택·태그 입력)로 조정할 수 있고, 요소 모서리·변을 직접 드래그해 가로·세로 크기를 바꿀 수 있습니다(드래그 크기 조정도 되돌리기/다시 실행에 반영). 반복 영역(목록처럼 같은 틀이 여러 번 펼쳐지는 자리)의 속성을 편집하면 모든 항목에 함께 적용됩니다. 요소를 편집하는 동안에는 선택한 요소만 밝게 유지되고 나머지 화면은 어둡게 잠겨, 다른 요소를 실수로 건드리지 않습니다. 속성 편집 팝업은 헤더를 잡아 옮길 수 있으며 선택한 요소를 가리지 않도록 빈 공간으로 자동 배치되고, 큰 요소에서도 요소 추가·메뉴 버튼이 크기 조절 핸들과 겹치지 않도록 여백을 둡니다. 고급 설정에서 요소를 볼 수 있는 권한을 권한 목록에서 골라 지정할 수 있습니다. 목록 있음·없음, 회원·비회원, 검증 실패·저장 중 같은 화면 상태를 미리보기로 전환해 데이터 의존 영역을 실제와 가깝게 검토할 수 있고, 같은 데이터 이름을 여러 확장이 쓰더라도 각 화면이 자기 출처의 샘플로 올바르게 표시됩니다. 라우트를 전환해도 저장하지 않은 편집 내용이 유지되며, 미저장 상태로 화면을 떠나거나 새로고침하면 경고가 표시됩니다. 마지막 저장 상태로 한 번에 되돌리는 초기화 버튼도 제공합니다.\n- 이미지 관리 — 배경 이미지 등 레이아웃 첨부 이미지를 업로드·관리할 수 있으며, 상단 툴바와 배경 이미지 컨트롤에서 썸네일 갤러리를 열어 업로드·삭제·배경 적용을 할 수 있으며, 지정한 배경 이미지가 편집 캔버스에 즉시 반영되고(채움·맞춤·타일 표시 모드 포함) 모달을 다시 열어도 현재 설정이 그대로 복원됩니다. 발행된 배경 이미지는 일반 방문자 화면에서도 정상적으로 표시됩니다.\n- 동작·표시조건·정렬 편집 — 요소를 선택하고 속성 편집 팝업에서 (1) 클릭·마우스 오버 시 동작을 \"다른 페이지로 이동\"·\"안내 메시지 보여주기\"·\"화면 상태 바꾸기\"·\"목록 새로고침\"·\"서버에 보내고 결과 처리\"·\"결제 진입\" 같은 친화 명칭으로 조립하고(서버 호출은 성공·실패 후속 동작까지 단계로 구성), (2) 로그인 회원·관리자·데이터 있음/없음·수정/새 화면·입력 오류 등 친화 조건을 \"그리고/또는\"으로 결합해 요소를 보여줄 조건을 정하며, (3) 가로·세로 컨테이너의 자식 배치(방향·정렬·줄바꿈·간격)와 정렬 박스 안 요소의 늘어남·정렬·순서를 시각 컨트롤로 편집할 수 있습니다. 핸들러·코드 용어 없이 친화 입력만 노출되며, 이동할 페이지·데이터 이름은 목록에서 선택합니다. \"결제 진입\" 동작은 특정 결제사에 고정하지 않고, 서버 응답이 알려주는 결제 수단(결제 핸들러·결제 데이터)을 데이터 검색에서 골라 연결합니다 — 결제 수단을 바꾸거나 추가해도 화면 편집 없이 동작합니다.\n- 색 모드·디바이스별 스타일 — 속성 편집에 라이트/다크 색 모드와 디바이스별 편집을 더해, 스타일·표시조건 탭 상단의 서브탭에서 라이트/다크 색 모드와 기본값·PC·태블릿·모바일(+ 직접 지정하는 커스텀 화면 폭)을 선택해, 각 범위에 스타일·정렬·표시 조건을 따로 지정할 수 있습니다. 글자색·배경색은 패널의 프리셋 색을 고르면 라이트와 다크 모두에 적용되어 다크 화면용 색을 비개발자도 지정할 수 있고(컬러 피커로 직접 고른 자유 색은 라이트 화면 전용), 어느 범위에 기본값과 다른 설정이 있으면 탭에 표시점이 뜨며 \"기본값으로 초기화\"로 그 범위만 되돌릴 수 있습니다. 편집기 상단 라이트/다크 토글로 미리보기 색상 테마를 전환하면, 관리자 화면이 다크 테마여도 미리보기가 라이트/다크를 정확히 구분해 보여줍니다.\n- 화면 문구 다국어 편집 — 상단 툴바의 콘텐츠 언어 선택으로 미리보기에 표시되는 화면 문구의 언어를 전환할 수 있고, 캔버스의 텍스트를 더블클릭하면 그 자리에서 바로 고칠 수 있습니다(Enter 로 확정, Esc 로 취소). 문구를 고치면 그 문구가 다국어 키로 자동 등록되어, 입력한 내용이 즉시 화면에 반영됩니다. 편집 중 떠 있는 서식 막대로 굵게·기울임·밑줄을 켜고 끄거나, 글자 크기·정렬·글자색을 목록에서 골라 적용할 수 있으며(색은 색상 미리보기와 함께 선택), 바뀐 서식은 편집 중에도 즉시 보입니다. 편집 안내 배지를 누르면 속성 편집의 번역 탭이 열려 모든 언어의 값을 한 번에 입력할 수 있어, 한 언어만 고치고 다른 언어 번역을 놓치는 일을 줄입니다. (데이터에 연결된 문구·반복 영역 문구는 직접 편집 대상에서 제외됩니다.)\n- 모든 컴포넌트로 속성 편집 확대 — 어떤 요소를 선택해도 속성 탭에 \"요소 ID\" 칸이 항상 제공되어 화면 내 요소에 고유 식별자를 부여할 수 있으며(화면 안전 문자만 입력되고 한글·공백 등은 자동으로 걸러집니다), 컴포넌트별 편집 항목(링크 주소·이미지 주소·입력칸 종류·선택지·정렬 옵션 등)도 친화 컨트롤로 편집할 수 있습니다. 아이콘은 자유 입력 대신 검색 가능한 그리드에서 골라 지정할 수 있어(템플릿이 제공하는 아이콘 목록을 검색으로 빠르게 찾기), 아이콘 이름을 외우지 않아도 됩니다. 입력칸의 선택지처럼 여러 항목으로 된 값은 항목 추가·삭제·순서 변경으로 편집하며, 이미 데이터에 연결된 값은 실수로 덮어쓰지 않도록 보호됩니다.\n- 모달 시각 편집 — 좌측 트리에 각 화면이 사용하는 모달 목록이 \"이 화면의 모달\" 그룹으로 표시되고(공통 레이아웃의 모달, 모듈·플러그인 화면의 모달, 별도 파일로 분리된 모달 포함), 각 모달은 \"주문 취소\"·\"비회원 조회 비밀번호 재설정\"처럼 친화 제목으로 표시됩니다. 모달을 선택하면 실제 화면 위에 모달이 열린 상태로 문구 수정·요소 추가·저장을 할 수 있습니다. 모달 바깥 영역은 어둡게 잠겨 클릭·드래그·편집이 차단되어 실수로 호스트 화면을 건드리지 않습니다.\n- 버전 배지·복원 — 좌측 트리에 각 화면과 확장 주입 조각의 현재 저장 버전 배지가 표시되고, 버전 기록 모달에서 이전 버전과의 비교(diff)와 복원을 확장 주입 조각에서도 동일하게 사용할 수 있으며, 저장·복원 시 배지가 새로고침 없이 즉시 갱신되고 복원 결과가 캔버스에 바로 반영됩니다.\n- 공통 레이아웃 편집 — 각 화면의 콘텐츠가 채워질 슬롯 자리가 점선 박스와 라벨로 표시되고(슬롯 자체는 편집 대상이 아니라 잠금), 헤더·푸터를 클릭해 바로 선택·편집할 수 있습니다. 모달 편집 중에도 편집 캔버스의 스크롤이 그대로 유지됩니다.\n- 화면 상태 미리보기 — 모달·공통 레이아웃까지 확대해, 배송지 직접 입력, 모바일 메뉴 펼침, 쿠키 배너 표시, 본인인증 진행 모달처럼 특정 조작에서만 나타나는 영역을 상태 전환만으로 미리보며 편집할 수 있고, 모달 안에 확장이 주입한 영역도 시각 편집할 수 있습니다.\n- 일부 템플릿(사용자 템플릿 등)의 화면을 미리볼 때 헤더가 \"데이터를 표시할 수 없습니다\"로 표시되지 않고, 미리보기용 샘플 데이터가 모든 템플릿에서 안전하게 렌더됩니다.\n\n- 회원이 아닌 사용자(비회원)에게도 이메일 알림을 보낼 수 있도록 알림 시스템을 확장했습니다. 회원에게 알림을 보내던 것과 동일한 방식으로, 입력한 이메일을 수신자로 알림이 발송되며 발송 내역에도 함께 기록됩니다. 알림 종류별로 비회원 발송을 허용할 수단을 선택할 수 있어, 이메일은 비회원에게 보내되 사이트 안 알림처럼 로그인이 필요한 수단은 보내지 않도록 구분됩니다(새 알림 수단이 추가될 때 비회원 허용 여부를 별도로 정하지 않으면 기본적으로 보내지 않습니다). 비회원에게 가는 알림은 그 사용자가 보던 화면 언어로 발송됩니다.\n\n### Changed\n\n- 모든 번들 플러그인이 위 표준 베이스 클래스로 이관되어 캐시·스토리지 도메인이 코어 및 다른 확장과 명확히 격리되도록 개선. 한 플러그인의 캐시 키가 코어 캐시 영역을 침범하던 잠재 결함을 차단하며, 관리자 화면의 변경사항이 의도대로 즉시 반영됩니다.\n- 템플릿이 정의하는 에러 페이지(404 / 403 / 500 / 503 / 401 / 점검) 의 식별자를 다른 모든 페이지와 동일한 디렉토리 접두사 포함 형식(`errors/{코드}`) 으로 통일. 템플릿 설치 시 에러 페이지가 일관된 식별자로 시드되며, 일부 환경에서 에러 페이지가 \"찾을 수 없음\" 응답으로 떨어지던 회귀를 차단합니다. 번들 템플릿(`sirsoft-basic`, `sirsoft-admin_basic`) 의 에러 페이지 설정도 함께 정합화됩니다.\n- 템플릿 외부 리소스 위임 — Font Awesome·웹폰트 등 외부 CDN 리소스를 페이지 코드에서 하드코딩하지 않고 템플릿의 `externals` 선언으로 위임. 외부 스타일시트·웹폰트·스크립트와 preconnect·preload 등 리소스 힌트를 한 곳에서 관리하며, 관리자 템플릿과 사용자 템플릿에 동일하게 적용\n- 템플릿 컴포넌트 에셋 캐시 버스팅 — `?v=time()` → `?v={{ $extensionCacheVersion }}`로 변경하여 매 요청 무효화 → 확장 변경 시점에만 무효화\n- 모듈·플러그인 업데이트 시 관리자가 편집한 확장 레이아웃을 보존하거나 덮어쓰는 전략을 선택할 수 있도록 개선.\n- 코어 업그레이드 스텝 명령에 보조 단계(권한 정상화·마이그레이션·캐시 정리 등)를 건너뛰고 스텝만 실행하는 옵션 추가.\n- 코어 영역의 프론트엔드 다국어 자원(에러 메시지·시스템 토스트 등) 위치를 정리하여 어떤 템플릿이 부팅되어도 자동 노출되도록 개선. 이전에 코어 에러 화면에서 `core.errors.*` 같은 키가 raw 로 노출되던 결함이 해소됩니다.\n\n### Fixed\n\n#### 본인인증(IDV)\n\n- 정책 관리에서 코어·모듈·플러그인이 제공한 정책의 \"인증 목적\"·\"적용 대상\"·\"우선순위\"를 바꿔 저장해도 반영되지 않던 문제를 수정했습니다 — 이제 정책 키·강제 시점·강제 위치(정책이 걸리는 지점)만 고정되고, 그 외 모든 항목을 운영자가 자유롭게 변경할 수 있으며, 변경한 값은 확장 업데이트 후에도 유지됩니다.\n- 한 화면에 본인인증 정책이 여러 개 걸리고 우선순위가 같을 때, 어느 정책이 먼저 적용될지 정해지지 않아 의도한 인증(예: 성인 인증)이 무시되고 다른 인증이 적용될 수 있던 결함을 수정했습니다. 이제 우선순위가 같아도 항상 동일한 정책이 적용되도록 적용 순서를 고정하고, 정책 추가·수정 화면에 우선순위 입력칸을 제공하며 같은 위치에 같은 우선순위의 활성 정책을 저장하려 하면 저장을 막고 안내합니다. 정책 목록에도 우선순위 열을 추가해 한눈에 확인할 수 있습니다.\n- 성인 인증 같은 추가 조건을 함께 요구할 때, 그 조건을 충족하지 못해 인증이 실패하면 실패 사유 안내와 \"본인 확인이 필요합니다\" 안내가 동시에 뜨던 문제를 수정했습니다. 이제 추가 조건 미충족으로 실패하면 그 사유 안내 하나만 표시되며, 일반 본인인증 실패의 안내는 종전대로 표시됩니다.\n- 환경설정의 기본 프로바이더 / 목적별 프로바이더 / 코드 유효시간 / 최대 시도 횟수 4개 항목이 admin UI 저장값을 무시하던 결함 수정. 변경한 환경설정값이 회원가입·비밀번호 재설정 등 모든 본인인증 흐름에 즉시 반영됩니다.\n- 환경설정 > 본인인증 > 목적별 프로바이더에서 일부 본인인증 목적(예: 성인인증)의 프로바이더를 지정해 저장하면 \"설정 저장에 실패했습니다\" 오류로 전혀 저장되지 않던 결함 수정. 이제 모든 본인인증 목적의 프로바이더 매핑이 정상 저장됩니다. 아울러 저장 검증에 실패하면 문제가 된 목적별 프로바이더 입력칸이 빨갛게 강조되고 그 아래에 사유가 표시되어, 어느 항목을 고쳐야 하는지 바로 확인할 수 있습니다.\n- 정책에 프로바이더가 지정되지 않은 경우 환경설정의 기본 프로바이더가 자동 적용되도록 보강. 운영자가 정책마다 프로바이더를 일일이 지정하지 않아도 환경설정 기본값으로 일관되게 동작합니다.\n- 코드 입력 모달의 \"남은 시도 횟수\" 표시가 환경설정값과 무관하게 항상 5회로 굳어 있던 결함 수정. 운영자가 환경설정에서 지정한 한도가 사용자 화면에 정확히 표시됩니다.\n- 인증 코드를 최대 횟수까지 잘못 입력해 시도가 소진되었을 때, 화면에 \"최대 시도 초과 — 다시 요청해주세요\" 안내 대신 일반 오류 문구(\"인증 코드가 올바르지 않습니다\")가 표시되던 결함 수정. 시도가 소진되면 재요청을 안내하는 메시지가 정확히 표시됩니다.\n- 본인인증을 사용하는 모듈·플러그인을 비활성화해도 해당 확장이 설정한 본인인증 요구가 계속 적용되던 결함 수정. 이제 확장을 비활성화하면 그 확장이 선언한 본인인증 정책도 함께 적용 해제되고, 다시 활성화하면 운영자가 조정한 설정을 유지한 채 정상 복구됩니다.\n\n#### 확장 동기화·캐시·레이아웃 주입\n\n- 확장 메뉴 동기화 시점에 운영자가 수정하지 않은 필드 (아이콘·이름·정렬·URL) 가 잘못 \"사용자 수정\" 으로 표시되어 이후 모듈/플러그인 정의값으로의 자동 동기화가 영구히 차단되던 결함 수정. 확장 재설치·업데이트 시 메뉴 정의 변경이 정상 반영됩니다.\n- 확장 역할 동기화 시점에 운영자가 수정하지 않은 필드 (역할 이름·설명) 가 잘못 \"사용자 수정\" 으로 표시되어 이후 모듈/플러그인 정의값으로의 자동 동기화가 영구히 차단되던 결함 수정. 메뉴 동기화와 동일 맥락으로, 확장 재설치·업데이트 시 역할 정의 변경이 정상 반영됩니다.\n- 모듈·플러그인의 레이아웃 확장이 대상이 아닌 템플릿(관리자 확장 ↔ 사용자 템플릿)에도 등록되던 결함 수정. 업그레이드 시 잘못 등록된 항목을 자동 정리하며, 이후로는 대상 레이아웃이 존재하는 템플릿에만 등록됩니다.\n- 모듈·플러그인이 공통 레이아웃(헤더·푸터 등 모든 화면이 공유하는 틀)에 주입한 UI·초기화 동작이 실제 화면에는 전혀 나타나지 않던 결함 수정. 개별 화면이 공통 레이아웃을 물려받을 때 그 \"물려받은 출처\" 정보가 사라져, 공통 레이아웃을 대상으로 한 확장 주입이 어느 화면에도 적용되지 않았습니다. 이제 화면이 물려받은 공통 레이아웃을 대상으로 한 확장도 정상 적용되어, 모듈이 설치되어 있으면 해당 UI(예: 헤더 통화 선택기)와 초기화 동작이 모든 관련 화면에 표시됩니다.\n- 확장(모듈·플러그인) 변경 후 라우트 트리·메뉴·다국어 응답이 옛 값으로 남아 있던 결함 수정. 이전에는 클라이언트가 캐시 버전 신호를 동반하지 않아 백엔드 캐시가 갱신 시점을 놓쳤습니다. 확장 설치/활성화/업데이트가 일어나면 다음 페이지 로드부터 새 값이 즉시 보입니다.\n- 확장을 업데이트하거나 레이아웃을 저장해도 변경 내용이 화면에 반영되지 않고 옛 내용이 계속 보이던 결함 수정. 캐시 갱신 신호가 기록되는 저장소와 읽히는 저장소가 환경설정에 따라 어긋날 수 있어, 올린 갱신 신호가 화면 쪽에서 보이지 않던 문제였습니다. 이제 갱신 신호가 항상 같은 저장소로 일관되게 기록·조회되어 확장 업데이트·레이아웃 변경이 다음 화면 로드부터 즉시 반영됩니다.\n- 코어 업데이트 시 사용자가 모듈·플러그인·템플릿·언어팩의 `_bundled` 폴더 아래에 직접 만들어 둔 커스텀 폴더·파일이 함께 삭제되던 결함 수정. 이제 코어 업데이트는 코어가 제공하는 번들 확장만 갱신하고, 사용자가 추가한 항목은 그대로 보존합니다. (#43 @bigmsg 님께서 제보해주셨습니다.)\n- 관리자 권한(`sudo`)으로 코어를 업데이트한 뒤, 모듈·플러그인·템플릿·언어팩을 업데이트하려 하면 \"백업 생성 중\" 단계에서 `mkdir(): Permission denied` 오류로 실패하던 결함 수정. 관리자 권한 업데이트가 백업 폴더와 업그레이드 기록 파일을 관리자 소유로 남겨, 이후 웹서버 계정이 같은 위치에 쓰지 못하던 문제였습니다. 이제 이들 폴더·파일의 소유권과 그룹 쓰기 권한이 정상화되고, 코어와 확장의 업그레이드 기록을 별도 파일로 분리해 두 작업이 서로의 기록 파일을 두고 충돌하지 않습니다.\n\n#### 알림\n\n- 회원에게 보내는 알림(이메일·사이트 알림)이 보내는 사람(관리자 등)의 화면 언어가 아니라 받는 회원 본인의 언어로 발송되도록 수정. 관리자가 한국어 화면에서 다른 언어를 쓰는 회원의 주문 상태를 바꿔도, 알림은 그 회원의 언어로 전달됩니다.\n- 신규 주문 접수 시 관리자에게 발송되는 알림 메일의 결제금액이 0원으로 표시되던 결함 수정. 결제 예정 금액이 정확히 표시됩니다.\n- 알림 템플릿을 기본값으로 복원할 때 제목·본문만 복원되고 수신자·클릭 주소는 관리자가 수정한 값이 그대로 남던 결함 수정. 이제 복원 시 수신자·클릭 주소까지 기본값으로 정확히 되돌아갑니다.\n\n#### 설치·인스톨러\n\n- 설치 마법사에서 데이터베이스 테이블 접두사를 지나치게 길게 입력하면 일부 확장 설치 시 테이블 인덱스 이름이 데이터베이스 한도를 넘겨 설치가 실패할 수 있던 문제를 예방합니다 — 접두사 입력 길이를 6자로 제한하고, 한도를 넘기면 설치 전 단계에서 안내합니다.\n- 인스톨러에서 \"기존 테이블 삭제 후 설치\"를 선택할 때, 입력한 테이블 접두사와 무관한 다른 테이블까지 모두 삭제되던 결함 수정. 이제 입력한 접두사로 시작하는 테이블만 삭제되어, 같은 데이터베이스를 다른 프로그램과 함께 쓰거나 다른 접두사로 재설치할 때 무관한 테이블이 보존됩니다.\n\n#### 관리자 화면·검색·시스템·보안\n\n- 관리자 대시보드의 최근 활동·모듈/플러그인/템플릿 상태·게시판/이커머스 위젯 등 같은 틀이 여러 번 펼쳐지는 목록에서, 항목마다 고유 식별자를 붙이도록 작성된 화면이 모든 행에 같은 식별자를 그대로 내보내 일부 보조 기능·접근성 도구가 항목을 구분하지 못하고 일부 항목이 화면에서 누락되어 보이던 결함을 수정했습니다 — 이제 반복 목록의 각 행이 고유하게 식별되어 모든 항목이 정상 표시됩니다.\n- 한 화면 안에서 탭이나 필터를 클릭으로 전환할 때, 화면에 따라 표시 조건에 묶인 데이터 목록이 비어 보이던 결함 수정. 새로고침해야 보이던 목록이 이제 탭·필터 전환 즉시 정상 표시됩니다. (표시 조건에 따라 로딩되는 데이터 목록이 전환 시점에 다시 평가되지 않던 문제였습니다.)\n- 환경설정 화면 등에서 별도 표시 텍스트 없이 켜고 끄는 토글·체크박스를 켜서 저장하면 \"참/거짓 값이어야 합니다\" 오류로 저장이 거부되던 결함 수정. 토글의 켜짐/꺼짐 상태가 문자열이 아닌 참/거짓 값으로 정확히 전달되어 정상적으로 저장됩니다.\n- 관리자 목록 화면에서 필터(발급상태·사용여부 등)를 선택하고 검색하면 일부 필터가 검색 직후 \"전체\"로 풀려 보이던 결함 수정. 새로고침해야 선택한 필터가 다시 표시되던 불편을 없애고, 검색 직후에도 선택한 필터가 그대로 유지됩니다.\n- 검색창에 `<`, `>`, `(`, `+` 등 특수문자를 입력하면 오류가 발생하던 결함 수정. 특수문자는 일반 검색어로 처리되어, 어떤 문자를 입력해도 오류 없이 검색 결과(또는 빈 결과)가 표시됩니다.\n- IME(한글·일본어·중국어) 입력 도중 Enter 로 글자를 조합 확정할 때 마지막 글자가 누락되거나 검색·제출이 두 번 실행되던 결함 수정. 이제 조합이 끝난 뒤의 Enter 만 동작합니다. (#54 @devrhee16 님께서 제보해주셨습니다.)\n- 관리자 대시보드의 시스템 리소스(CPU·메모리·디스크) 수집이 일부 호스팅 환경(open_basedir 제한, 일부 시스템 함수 비활성)에서 실패할 때 대시보드 리소스 조회 전체가 오류로 응답하던 결함 수정. 이제 수집할 수 없는 항목만 \"알 수 없음\"으로 표시되고 나머지 화면은 정상 동작합니다. (#40 @glitter-gim 님께서 제보해주셨습니다.)\n- 시스템 정보 화면의 디스크 사용량 조회도 동일한 환경에서 안전하게 \"알 수 없음\"으로 표시되도록 보강했습니다.\n- 웹소켓 사용을 끈 상태에서도 실시간 연결이 일부 동작할 수 있던 문제 수정. 이제 웹소켓 사용을 끄면 실시간 연결·채널 인증이 완전히 비활성화됩니다. (#50 @bigmsg 님께서 제보해주셨습니다.)\n- 사이트 이름을 언어별로 다르게 저장한 환경에서 브라우저 탭 제목과 검색 메타데이터(JSON-LD)가 올바르게 표시되지 않거나 페이지가 깨질 수 있던 문제 수정. 이제 모든 메타 경로가 현재 언어의 사이트 이름을 일관되게 사용합니다. (#49 @glitter-gim 님께서 제보해주셨습니다.)\n- 존재하지 않는 페이지에 접근할 때 정상 페이지처럼 200 응답을 반환하던 문제 수정. 이제 미등록 경로는 올바른 404 응답을 반환해 검색엔진이 없는 페이지를 색인하지 않습니다. (#47 @glitter-gim 님께서 제보해주셨습니다.)\n- 인증이 필요한 관리자 API에 로그인 없이 접근할 때, 브라우저 주소창 직접 입력 등 일부 요청에서 서버 오류로 응답하던 결함 수정. 이제 어떤 방식으로 접근해도 일관되게 \"인증 필요\" 응답이 반환됩니다. (#39 @glitter-gim 님께서 제보해주셨습니다.)\n\n### Security\n\n- 사용되지 않는 파일 서빙 엔드포인트(`/storage` 직접 업로드·다운로드 경로) 노출을 제거했습니다. 실제 파일 업로드·다운로드는 별도의 인증된 경로로 처리되며 영향이 없습니다. (#52 @glitter-gim 님께서 제보해주셨습니다.)\n- 설치가 완료된 시스템에서 설치 상태 관리 엔드포인트(`/install/api/state-management.php`)가 다른 설치 API 와 동일하게 차단되지 않아, 설치 완료 후에도 설치 상태 조회·초기화·중단 요청이 가능하던 문제를 수정했습니다. 설치가 완전히 끝난 환경에서는 해당 요청이 모두 HTTP 410 으로 차단됩니다. 설치 진행 중 완료 화면으로 정상 전환되는 동작은 영향받지 않습니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1056)\n\n### Removed\n\n- 기본 제공 결제 수단을 KG이니시스로 단일화했습니다. 그 외 결제 연동(나이스페이먼츠·NHN KCP·토스페이먼츠)은 기본 번들에서 제외되며, 필요한 경우 별도 플러그인으로 설치해 사용할 수 있습니다. 이미 해당 결제 연동을 설치해 사용 중인 사이트는 영향받지 않습니다.\n\n## [7.0.0-beta.7] - 2026-05-15\n\n### Fixed\n\n- 인스톨러 설치 완료 후 `.env` 에 DB 자격증명과 `APP_KEY` 가 기록되지 않고 `storage/installer/runtime.php` 에 평문으로 잔존하던 결함을 수정. 운영 동작은 정상이었으나 자격증명이 정상적인 위치로 옮겨지지 않고 임시 파일에 보존되는 상태였습니다. (#37 @glitter-gim 님께서 제보해주셨습니다.)\n- beta.4~beta.6 출시본에서 위 결함으로 인해 자격증명이 잔존한 환경을 beta.7 업그레이드 시 자동으로 감지하여 `.env` 로 머지하고 임시 파일을 정리하도록 업그레이드 스텝을 추가했습니다. `.env` 의 `INSTALLER_COMPLETED` 마커가 이미 설정된 정합 이상 케이스는 자동 정리 대상에서 제외하고 운영자 수동 검토를 안내합니다.\n- 위 업그레이드 스텝이 `.env` 를 머지할 때 권한·소유자·그룹을 웹 서버 기준으로 정렬하도록 보강. 기존 `.env` 가 있던 환경에서는 운영자가 설정한 권한을 그대로 보존하고, 새로 생성되는 환경에서는 웹 서버가 읽을 수 있는 권한과 그룹을 자동으로 적용합니다. 명령줄 업그레이드 실행 사용자와 웹 서버 사용자가 다른 자체 구축 서버 환경에서 머지 직후 웹 서버가 `.env` 를 읽지 못해 사이트가 다운되는 회귀를 차단합니다.\n- 인스톨러 설치 완료 화면의 `.env` 권한 강화 안내(`chmod`/`chgrp` 명령 예시) 가 Windows 환경에서 비노출되도록 OS 가드 추가. POSIX 권한 모델이 없는 Windows 환경에서 무의미한 Linux/macOS 전용 명령이 노출되어 운영자 혼란을 유발하던 문제를 수정합니다.\n- beta.4~6 출시본의 인스톨러 결함으로 자격증명이 `storage/installer/runtime.php` 에 잔존한 환경에서 beta.7 업그레이드 시 번들 확장 일괄 업데이트 단계가 `Access denied for user 'root'@'localhost'` 로 실패하던 회귀 수정. 업그레이드 스텝이 `.env` 머지 직후 부모 프로세스의 환경 변수도 함께 갱신하여 후속 자식 프로세스가 정상 자격증명을 상속받도록 보강합니다. 정상 설치 환경 (beta.3 등) 에서는 트리거되지 않습니다.\n- 위 동일 회귀의 추가 차단 — 코어 업데이트가 별도 프로세스로 업그레이드 스텝을 실행하는 경로에서는 자식 종료 후 부모의 환경 변수가 갱신되지 않아 동일 증상이 재발하던 결함 수정. 자식 프로세스가 부팅 시점에 디스크의 `.env` 와 자신의 환경 변수를 대조하여 손상이 감지되면 디스크 값으로 보정하도록 보강하여, 다음 단계의 확장 일괄 업데이트가 정상 자격증명으로 진행되도록 합니다. 정상 설치 환경에서는 자동 비활성화됩니다.\n\n### Changed\n\n- `INSTALL.md` 에 \"업그레이드\" 섹션을 신설하여 자동 업데이트의 표준 절차, `sudo` 권장 사유 (`.env` · `bootstrap/cache` 의 권한 모델), 사용자군별 단계적 업그레이드 분기, 권한·캐시 트러블슈팅을 한곳에서 안내하도록 보완. 관리자 환경설정 > 정보 > 업데이트 확인 모달이 본 섹션으로 외부 링크를 노출합니다. (#30 @yks118 님께서 건의해주셨습니다.)\n- 관리자 환경설정 > 정보 > 업데이트 확인 모달의 자동 업데이트 명령어 안내를 `sudo php artisan core:update` 권장 형태로 변경. 권한이 일치하지 않는 환경(`.env` 가 웹 서버 사용자 소유) 에서의 실행 실패를 사용자가 사전에 회피할 수 있도록 모달 하단에 \"전체 업그레이드 가이드 보기\" 외부 링크를 추가해 INSTALL.md 의 업그레이드 섹션으로 연결합니다. (#30 @yks118 님께서 제보해주셨습니다.)\n\n### Notes — `.env` 권한 권장안\n\n위 업그레이드 스텝은 `.env` 가 이미 존재하는 환경에서는 권한을 임의로 변경하지 않고 기존 상태를 그대로 보존합니다. 보안 위생 강화를 원하시면 다음 권장안을 참고하세요.\n\n- 가장 안전한 권한은 `0600` (소유자만 읽기·쓰기) 입니다. `.env` 는 DB 비밀번호와 `APP_KEY` 등 평문 자격증명을 포함하므로 외부 접근을 최소화하는 것이 권장됩니다.\n- 단, `0600` 적용 전에 **`.env` 의 소유자가 웹 서버 실행 사용자와 동일해야** 합니다 (예: 단일 사용자 환경의 `chown www-data:www-data .env && chmod 600 .env`). 소유자가 웹 서버 사용자와 다른데 `0600` 으로 변경하면 웹 서버가 `.env` 를 읽지 못해 사이트가 응답하지 않습니다.\n- 명령줄 사용자와 웹 서버 사용자가 다른 환경 (자체 구축 서버 등) 에서는 `0640` 권한 + 그룹을 웹 서버 그룹으로 설정 (`chgrp www-data .env && chmod 640 .env`) 하는 것이 안전한 절충안입니다. 웹 서버는 그룹 멤버로 읽고, 명령줄 사용자는 직접 수정할 수 있습니다.\n\n## [7.0.0-beta.6] - 2026-05-14\n\n### Fixed\n\n- 코어 업데이트 자동 롤백 시 신 버전이 추가한 신규 파일이 활성 디렉토리에 잔존하여 `BindingResolutionException` 등 부정합 부팅 실패가 발생하던 결함 수정. 백업 디렉토리에 신규 파일 manifest 를 기록하고 롤백 시 정확히 그 목록만 정리합니다. 사용자가 코어 영역에 직접 추가한 파일과 모든 symlink (`public/storage` 등) 는 보존됩니다. (#34 @bigmsg 님께서 제보해주셨습니다.)\n- 자동 롤백 후 `bootstrap/cache` 잔존 PHP 캐시로 인한 추가 부팅 실패 차단 — 롤백 직후 캐시를 자동으로 정리합니다.\n- beta.5 이전 자동 롤백으로 인해 활성 디렉토리에 잔존했을 수 있는 ServiceProvider 후보를 beta.6 업그레이드 스텝이 진단 로그로 식별하여 운영자 수동 검토를 안내합니다. 자동 삭제하지 않으므로 사용자가 직접 추가한 ServiceProvider 는 보존됩니다.\n- 업그레이드 스텝 단독 실행 명령(`core:execute-upgrade-steps`) 이 마이그레이션·코어 재동기화·버전 갱신·캐시 정리·번들 확장 일괄 업데이트를 자동으로 함께 수행하도록 보강. 자동 업데이트 중단 후 안내된 수동 명령을 그대로 실행하면 별도 보조 작업 없이 업그레이드가 완료됩니다.\n- 인스톨러 Step 3 (PHP CLI / Composer / 자동 감지 / 코어 _pending 경로 검증) 의 프론트엔드 catch 분기에서 사용하는 다국어 키가 언어 파일에 정의되어 있지 않아, 서버 응답 실패 시 화면에 다국어 키 문자열이 그대로 노출되던 결함 수정.\n- 인스톨러 다국어 파일에 중복 정의된 키 4건 정리. POST 전용 API 의 메서드 거부 메시지가 일반 문구로 덮여 표시되던 문제, 데이터베이스 연결 실패 로그에서 placeholder 가 치환되지 않던 문제, HTTPS 카드 라벨이 안내 문장으로 표시되던 문제가 함께 해결됩니다.\n- 인스톨러 Step 3 의 PHP CLI 및 Composer 절대경로 검증이 공유 호스팅 환경(시놀로지 DSM 등 `open_basedir` 가 시스템 binary 영역을 차단하는 환경)과 Windows 환경에서 정상 경로임에도 거부되던 결함 수정. 셸 인자 escape 와 메타문자 차단은 유지하고, 파일 시스템 권한에 의존하던 사전 검사를 실제 실행 결과로 판정하도록 변경합니다. Windows 의 백슬래시 경로 구분자도 정상 입력으로 인식됩니다 (단, `C:\\Program Files\\...` 같은 공백 포함 디렉토리는 공백 없는 경로 또는 8.3 short path 사용 권장). (#33 @glitter-gim 님께서 제보해주셨습니다.)\n- 코어 운영 단계의 composer 바이너리 자동 인식이 같은 `open_basedir` 환경에서 실패하여 모듈/플러그인 설치 시 vendor-bundle 모드만 사용 가능하던 결함 수정. composer 가 정상 설치되어 PATH 에 존재하는 환경에서도 인식되도록 검출 로직을 개선했습니다.\n- 멀티 PHP 버전 환경(시놀로지 DSM Web Station, cPanel/Plesk multi-PHP 등) 에서 Composer 를 특정 PHP 인터프리터로 실행하려는 \"PHP 절대경로 + Composer 절대경로\" 공백 분리 입력 형식이 거부되던 결함 수정. 입력을 두 토큰으로 분리한 뒤 각 토큰을 개별 escape 처리하여 안전을 유지하면서 멀티 PHP 운영 시나리오를 지원합니다.\n- 인스톨러 세션 쿠키가 일부 환경(비표준 포트 + dynamic DNS 도메인 + 브라우저 추적 보호 등) 에서 브라우저에 의해 차단되어 첫 화면 \"설치하기\" 클릭이 동작하지 않고 새로고침처럼 보이던 결함을 보완. 세션 쿠키 SameSite 정책을 Strict → Lax 로 완화하여 차단 케이스 자체를 줄이고, Step 0 진입 시 쿠키 round-trip 사전 진단을 수행하여 차단이 감지되면 사용자에게 회피 방법(다른 브라우저 / 쿠키 차단 해제 / IP·localhost 접속) 을 명시 안내합니다.\n\n### Added\n\n- `hotfix:rollback-stale-files` Artisan 커맨드 추가 — 자동 롤백 후 활성 디렉토리에 잔존할 수 있는 신 파일을 운영자가 명시 실행하여 진단/정리할 수 있는 단발성 회복 도구. 기본은 진단 모드 (실제 삭제 없음), `--prune` 옵션 시 확인 프롬프트 후 정리. symlink 와 보호 경로(`storage/`, `vendor/`, `.env*` 등) 는 자동 제외.\n- 본인인증 메시지 정의 목록 API 응답에 `can_create` 권한 키 추가 — 운영자의 수정 권한 보유 여부에 따라 관리자 화면이 \"정의 추가\" 버튼을 정확히 활성화/비활성화할 수 있도록 보강.\n\n## [7.0.0-beta.5] - 2026-05-12\n\n### Upgrade Notice\n\nbeta.4 → beta.5 업그레이드 시 beta.4 의 결함으로 인해 활성 확장 디렉토리 일부가 손실됩니다. (#34 @bigmsg 님께서 제보해주셨습니다.)\n\n- 손실 항목 1: 운영자가 활성 디렉토리(`modules/{id}`, `plugins/{id}`, `templates/{id}`, `lang-packs/{id}`)에서 직접 수정한 코드\n- 손실 항목 2: 외부 install 한 확장 (GitHub 등 `_bundled` 에 포함되지 않은 모듈/플러그인/템플릿/언어팩)\n\nbeta.5 의 자동 복구 스텝이 `_bundled` 기반으로 번들 확장은 복원하지만, 위 두 항목은 `_bundled` 에 없어 자동 복구가 불가능합니다.\n\n업그레이드 전에 다음 디렉토리 전체를 별도 위치(외장 디스크, 별도 git 브랜치, tar.gz 파일)로 백업해두실 것을 권고합니다.\n\n- `modules/`\n- `plugins/`\n- `templates/`\n- `lang-packs/`\n\n업그레이드 후 자동 복구된 활성 디렉토리에 본인의 수정 사항/외부 확장이 누락되어 있다면, 사전 백업에서 복원하거나 외부 확장은 원본 소스(GitHub 등)에서 재설치하세요. 실패한 외부 확장 식별자는 업그레이드 로그(`storage/logs/core_update_success_*.log`)에 명시됩니다.\n\n#### 3가지 사용자군별 업그레이드 절차\n\n- **사용자군 B — 정상 beta.4 사용자 (대다수)**: `php artisan core:update` 표준 절차. beta.5 의 자동 복구 스텝이 활성 확장 디렉토리 + lang-packs 정합성을 보장합니다.\n\n- **사용자군 A — beta.3 → beta.4 업그레이드 도중 `Call to undefined method` 류 fatal 후 stuck 한 사용자**: `.env` `APP_VERSION` 을 변경하지 마시고 그대로 `php artisan core:update --force` 를 재실행하면 됩니다. 디스크는 이미 beta.4 이므로 새 자식 프로세스가 beta.5 메모리로 부팅되어 남은 업그레이드 스텝이 멱등 적용됩니다. 단, 활성 디렉토리에서 직접 수정한 코드 또는 `_bundled` 에 없는 외부 확장은 자동 복구 대상이 아니므로 업그레이드 전 위 디렉토리 백업이 동일하게 권장됩니다.\n\n- **사용자군 C — beta.1 또는 beta.2 사용자**: `php artisan core:update` 1회 호출로 beta.5 까지 직접 업그레이드는 권장하지 않습니다. 중간 버전의 업그레이드 스텝이 이전 버전 메모리에 없는 신규 코드에 의존하여 fatal 위험이 있습니다. 다음 단계적 업그레이드를 권장합니다:\n\n ```text\n beta.1 사용자: beta.1 → beta.2 → beta.3 → beta.4 → beta.5 (4 회)\n beta.2 사용자: beta.2 → beta.3 → beta.4 → beta.5 (3 회)\n ```\n\n 각 단계마다 `.env` `APP_VERSION` 이 자동 갱신되므로 다음 호출은 그대로 `php artisan core:update` 실행하면 됩니다. 가까운 릴리즈 zip 으로 `--source=` 또는 `--zip=` 옵션을 사용해 단계별 진행 가능합니다.\n\n#### 자동 롤백 후 `public/storage` 회복\n\n업그레이드 도중 자동 롤백 후 화면에서 업로드 파일이 보이지 않거나 미디어 경로가 깨졌다면 `public/storage` symlink 가 일반 디렉토리로 변질된 상태일 수 있습니다.\n\n**우선 시도 — beta.5 의 자동 복구**: beta.4 → beta.5 업그레이드 자체는 부모 beta.4 메모리 결함으로 symlink 가 깨질 수 있으나, beta.5 의 업그레이드 스텝이 다음 패턴을 자동 감지하여 복구합니다:\n\n- 감지 조건: `public/storage` 가 일반 디렉토리 + `storage/app/public` 이 존재 (Laravel 표준 구성)\n- 복구 동작: 손상된 디렉토리를 `public/storage.broken.YYYYMMDDHHMMSS` 로 보존 후 symlink 재생성\n- 운영자 조치: 자동 복구 후 `.broken` 디렉토리의 콘텐츠를 검증한 뒤 수동 삭제 (false positive 방어를 위해 자동 삭제하지 않음)\n\n**자동 복구가 작동하지 않은 경우 매뉴얼 회복**:\n\n```bash\nrm -rf public/storage\nphp artisan storage:link\n```\n\nbeta.5 부터 백업/복원 과정에서도 symlink 가 보존되므로 beta.5 → beta.6 이후로는 본 결함이 자동 차단됩니다. Windows 환경에서는 PHP `symlink()` 가 `SeCreateSymbolicLink` 권한을 요구하므로 자동 복구가 silent skip 될 수 있어 매뉴얼 회복 절차가 여전히 필요합니다.\n\n#### spawn 자식 프로세스 실패 시 동작 모드\n\nbeta.5 부터 코어 업데이트의 자식 프로세스 spawn 이 실패하면 즉시 abort 가 기본 동작입니다 (`G7_UPDATE_SPAWN_FAILURE_MODE=abort`). 다음 4가지 상황에서 부모 프로세스 메모리의 stale 클래스로 인한 fatal 위험을 차단합니다:\n\n1. `proc_open` 비활성 (일부 공유 호스팅)\n2. `proc_open` 자원 생성 실패\n3. 자식 비정상 종료 (uncaught exception)\n4. 자식 exit=0 이지만 업그레이드 스텝 0건 실행 (silent skip)\n\nabort 발생 시 운영자에게 수동 재개 명령 (`php artisan core:execute-upgrade-steps --from=... --to=...`) 을 안내합니다. `proc_open` 비활성 환경 등 호환성이 필요한 경우 `.env` 에 `G7_UPDATE_SPAWN_FAILURE_MODE=fallback` 을 설정하여 in-process fallback 으로 진행할 수 있으나, 부모 메모리 stale 로 인한 fatal 위험이 잔존합니다.\n\n### Added\n\n- 본인인증 관리자 권한을 조회/수정으로 분리 — \"프로바이더 설정 조회\" / \"정책 조회\" 권한 신설로 단순 조회 권한만 부여받은 운영자도 환경설정 본인인증 화면에 접근 가능 (기존에는 수정 권한이 없으면 조회 화면도 403)\n- 환경설정의 본인인증 정책/메시지 탭에 권한 기반 버튼 비활성화 적용 — 수정 권한이 없는 운영자에게는 \"정책 추가\" / \"수정\" / \"삭제\" / \"활성 토글\" 버튼이 자동으로 비활성화 (이커머스/게시판 본인인증 탭 동일)\n- 코어 업데이트의 spawn 자식 프로세스 실패 시 abort/fallback 동작 모드 선택 (`G7_UPDATE_SPAWN_FAILURE_MODE`, 기본 `abort`) — 부모 메모리 stale 로 인한 upgrade step fatal 위험을 fail-fast 로 차단 (#28 @bigmsg 님께서 제보해주셨습니다.)\n- 업그레이드 스텝에 \"버전별 데이터 스냅샷\" 규약 도입 — 멀티 버전 점프 시에도 사용자가 단계별로 업그레이드한 것과 동등한 결과를 보장하도록 각 스텝의 시드 카탈로그·적용 로직·단발성 핫픽스를 해당 버전 디렉토리로 격리. 외부 확장 작성자가 beta.5+ 신규 업그레이드 스텝 작성 시 적용 (상세: 업그레이드 스텝 가이드) (#29 @bigmsg 님께서 건의해주셨습니다.)\n- 언어팩 일괄 활성화 검증 규칙에 동적 확장 훅 추가 (`core.language_packs.bulk_activate_validation_rules`) — 모듈/플러그인이 호스트 확장 재활성화 cascade 흐름에서 추가 검증 필드를 동적으로 등록 가능\n\n### Changed\n\n#### Breaking\n\n- 본인인증 관리자 권한 키 정리 — 기존 `core.admin.identity.manage` / `core.admin.identity.policies.manage` 가 각각 `core.admin.identity.providers.update` / `core.admin.identity.policies.update` 로 변경. beta.4 환경에서 기존 권한을 부여받은 운영자 역할은 업그레이드 시 자동으로 새 권한 키로 마이그레이션 (부여 시점/부여자/스코프 메타데이터 모두 보존). 외부 확장 또는 운영 도구가 옛 키 문자열을 하드코딩한 경우 새 키로 갱신 필요\n\n### Fixed\n\n- 코어 업데이트 시 활성 모듈/플러그인/템플릿/언어팩 디렉토리가 삭제되던 문제 수정 — beta.4 → beta.5 업그레이드 시 번들 원본에서 활성 디렉토리를 자동 복원하고, 외부 설치 확장은 업데이트 로그로 재설치 안내\n- 코어 업데이트의 단계 전환 신호를 비정상 입력으로부터 차단하도록 검증 강화\n- 본인인증 정책 응답의 생성/수정 일시가 사용자 타임존이 아닌 UTC ISO 문자열로 노출되던 문제 수정\n- 백업/롤백 시 `public/storage` 등 symlink 가 target 디렉토리 내용으로 추적 복사되어 symlink 가 일반 디렉토리로 변질되던 문제 수정 (Linux 환경 한정 — Windows 는 권한 한계로 일반 디렉토리 폴백 + 수동 회복 안내 유지)\n- root/super user 환경에서 Composer 검증·설치가 비대화형 컨텍스트에서 실패하던 문제 수정 (코어 업데이트, 확장 의존성 설치, 인스톨러 포함) (#31 @glitter-gim 님께서 제보해주셨습니다.)\n- 본인인증 활동 로그의 \"액션\" 열에 `identity.verify` / `identity.verify_failed` 가 번역되지 않은 키 그대로 노출되던 문제 수정 — 코어 액션 라벨 매핑이 누락되어 있던 부분을 추가\n- 본인인증 단계에서 운영자가 지정한 본인인증 수단이 무시되고 항상 기본 수단(메일)으로 발행되던 문제 수정 — 가입 정책에 설정한 본인인증 수단과 API 요청에서 명시한 수단이 실제 인증 발행에 반영되도록 변경\n\n## [7.0.0-beta.4] - 2026-05-11\n\n### Changed\n\n#### Breaking\n\n- 레이아웃 `navigate` 액션의 기본 스크롤 동작이 페이지 이동 후 최상단 이동으로 변경됨 — 일반 하이퍼링크 이동 UX 와 일치. 이전처럼 스크롤 위치를 유지하려면 `scroll: \"preserve\"` 명시 필요. 검색 필터/페이지네이션 등 위치 유지가 필요한 화면에서 회귀가 의심되면 해당 옵션 추가 (engine-v1.45.0)\n\n#### 언어팩 매니페스트 정합화\n\n- 언어팩 매니페스트(`language-pack.json`) 를 모듈/플러그인/템플릿 매니페스트와 동일한 필드 구조로 정렬 — 외부 작성자가 다른 확장과 동일한 표준으로 언어팩을 만들 수 있도록 개선\n- 언어팩 매니페스트에 GitHub 저장소 필드 추가로 GitHub 기반 업데이트 경로 지원 (모듈/플러그인 매니페스트와 동일한 동작)\n- 관리자 환경설정의 언어팩 카드와 상세 모달이 모듈/플러그인/템플릿 카드와 동일한 형식으로 다국어 이름 · 설명 · GitHub 링크를 노출하도록 변경\n\n### Added\n\n#### Generator 메타 태그\n\n- 관리자 환경설정 → SEO 탭에 Generator 메타 태그 카드 추가 — 토글로 노출 여부를 제어하고 내용 입력으로 W3Techs 등 CMS 시장 점유율 측정 도구가 인식하는 `` 태그를 SEO 봇 페이지·SPA·관리자 셸 모두에 출력. 내용 미입력 시 \"GnuBoard7 {버전}\" 자동 적용, 운영자가 버전 노출을 원치 않으면 \"GnuBoard7\" 만 입력 가능 (#26 @Lastorder-DC 님께서 건의해주셨습니다.)\n\n#### 웹 인스톨러 — 번들 언어팩 동반 선택 · 설치\n\n- 인스톨러 4단계 확장 선택 화면에 \"언어팩\" 카드 신설 — 번들 언어팩을 locale 별 서브헤딩으로 노출하고, 사용자가 모듈/플러그인/템플릿을 선택하면 그 확장에 종속된 번들 언어팩 카드가 즉시 활성화. 종속 확장을 해제하면 그 언어팩 카드는 자동으로 비활성화 + 선택 해제\n- 5단계 설치 진행 시 모든 확장의 install/activate 가 끝난 뒤 선택된 번들 언어팩을 일괄 설치 — 1건 실패는 best-effort 처리로 전체 설치를 중단하지 않음\n- 코어/확장당 다수 locale(일본어 + 중국어 등)이 동시에 번들된 시나리오 대응 — 각 locale 은 독립된 서브헤딩 + 카드로 노출\n\n#### 인스톨러 SSE 호환성 자동 감지\n\n- 인스톨러 시작 시 SSE 호환성을 사전 점검하여 환경에 맞는 모드(SSE 또는 폴링)로 단방향 진입 — 워커 동시 실행 race(테이블 / unique 키 중복 에러) 차단\n- SSE 비호환 환경 사용자에게 폴링 모드 진행 여부를 명시적 다이얼로그로 확인 후 시작\n\n#### ActivityLog 다국어 영역 분리\n\n- 활동 로그의 액션 라벨이 모듈/플러그인 자체 다국어 파일에서 우선 해석되도록 변경 — 그동안 코어에 일괄 등록되어야 했던 모듈 origin 라벨(이커머스 주문·상품·마일리지, 게시판 게시물·댓글·신고, 페이지 등) 이 각 영역으로 이전되어 모듈/플러그인이 자기 도메인 라벨을 자기 영역에서 자기설명. 미정의 시 코어 라벨로 자동 fallback 하여 회귀 없음\n- 모듈/플러그인이 발화하는 활동 로그가 호출자/대상 모델의 영역을 자동으로 인식하여 자기 다국어 파일로 라우팅 — 새 활동 로그를 추가할 때 별도 분기 코드 없이 자기 lang 만 채우면 정합\n\n#### 다국어 시더 인프라\n\n- 확장 entity 시더가 활성 언어팩의 다국어 데이터를 자동 머지하도록 다국어 시더 인터페이스 도입 — 신규 시더 작성 시 회귀를 자동 차단\n- 번들 일본어 언어팩 빌드 스크립트가 시더 메타데이터를 일관된 경로로 조회하도록 단순화\n\n#### 다국어 라벨 helper + Provider/Registry 페이로드 정합화\n\n- 다국어 라벨 표시 시 활성 언어팩의 lang key fallback 을 자동으로 처리하는 다국어 라벨 보강 helper 추가 — Provider/Registry 등록 페이로드(알림 채널·결제 PG 등) 와 settings JSON 다국어 데이터를 단일 시그니처로 처리\n- 알림 채널(`config/notification.php` 의 `default_channels`) 의 라벨이 활성 언어팩(일본어 등) 으로 자동 보강되도록 변경 — 일본어 활성 시 한국어 fallback 으로 노출되던 회귀 차단\n- 토스페이먼츠 PG 프로바이더 등록 페이로드가 lang key 기반으로 라벨을 선언하도록 변경 — 활성 언어팩으로 자동 보강\n- Provider/Registry 등록 페이로드가 다국어 JSON(`['ko' => ..., 'en' => ...]`) 을 직접 보유하지 않도록 audit 룰 신설 (회귀 자동 차단)\n- settings JSON 다국어 entry 의 식별 키(code/id/key) 보유 검증 audit 룰 신설 (lang pack fallback 키 조립 가능성 보장)\n- 프론트엔드 `$localized()` 표현식이 두 번째 인수로 lang key fallback 을 받도록 확장 — 활성 로케일 라벨 부재 시 모듈/플러그인 언어팩의 키로 자동 보강\n- 이커머스 환경설정 카탈로그(배송 가능 국가 / 통화 / 결제수단) 표시 시 활성 언어팩의 라벨이 자동 적용되도록 변경 — 일본어 활성 시 카탈로그 라벨이 한국어로 노출되던 회귀 차단 (다음 일본어 언어팩 빌드 시 자동 적용)\n- audit 룰이 `getDefault*Channels/Providers/Methods` 등 registry 기본값 반환 메서드의 다국어 JSON 직접 보유도 감지하도록 강화\n\n#### Settings 카탈로그 다국어 자동 보강\n\n- 환경설정 카탈로그(결제수단·통화·배송 가능 국가 등) 의 다국어 라벨이 카탈로그 빌드 시점에 활성 언어팩으로 자동 보강되도록 변경 — 사용자 체크아웃 + 관리자 환경설정 양쪽 모두 일본어 등 활성 시 한국어 fallback 으로 노출되던 회귀 차단\n- 모듈/플러그인 개발자가 카탈로그를 추가할 때 누락하지 않도록 audit 룰이 helper 호출 누락 검출\n- [docs/backend/settings-multilingual-enrichment.md](docs/backend/settings-multilingual-enrichment.md) 에 단순 helper 사용 패턴 문서화\n\n#### 확장 시스템\n\n- manifest `hidden: true` 플래그 추가 — 관리자 UI 목록에서 학습용/내부용 확장을 숨김 (CLI 는 정상 노출). 관리자 UI 에 슈퍼관리자 전용 \"숨김 포함\" 토글, artisan `module:list` / `plugin:list` / `template:list` 에 `--hidden` 플래그, 관리자 API `/api/admin/{modules,plugins,templates}` 에 `include_hidden` 쿼리 파라미터 지원\n- 학습용 최소 샘플 확장 4종 번들 추가\n - 모듈: `gnuboard7-hello_module` (Memo CRUD + 훅 발행 시연)\n - 플러그인: `gnuboard7-hello_plugin` (Action/Filter 훅 구독 시연)\n - Admin 템플릿: `gnuboard7-hello_admin_template` (Basic 컴포넌트 최소 셋)\n - User 템플릿: `gnuboard7-hello_user_template` (홈 + Memo 리스트 연동)\n- 모듈/플러그인/템플릿 정보 모달에 \"지원 언어\" 섹션 추가 — 코어/번들/사용자설치 출처 배지로 한눈에 확인\n- 모듈/플러그인/템플릿 인스톨러에 의존 확장 + 동반 번들 언어팩 동반선택 UI 추가 — 미선택 의존성에 종속된 언어팩은 자동 비활성화\n- 인스톨러 요구사항 검증 단계에 언어팩 디렉토리 쓰기 권한 점검 + 권한 부여 안내 추가\n- 사용자 수동 비활성화와 코어 버전 호환성으로 인한 자동 비활성화를 DB 수준에서 구분 — 자동 비활성화된 확장만 재호환 감지/원클릭 복구 대상이 되도록 분리 (#18 @laelbe 님께서 제보해주셨습니다.)\n- 코어 업그레이드 후 자동 비활성화 확장이 다시 호환되면 관리자 대시보드에 \"다시 활성화\" 알림 표시 + 원클릭 복구 버튼 제공 (자동 재활성화는 하지 않음 — 운영자가 명시적으로 복구)\n- 모듈/플러그인/템플릿 목록 화면 상단에 자동 비활성화 확장 안내 배너 추가 (코어 업그레이드 가이드 링크 동반)\n- 업데이트 모달에 코어 버전 호환성 안내 + \"위험을 이해하고 강제로 진행\" 체크박스 추가 — 운영자가 위험을 인지하면 비호환 확장도 강제 설치 가능\n- 관리자 대시보드 \"시스템 알림\" 카드 노출 — 코어 호환성 자동 비활성화/재호환 알림이 분기 렌더되며 개별 dismiss 지원\n\n#### 코어 업데이트 가시성\n\n- 코어 업데이트 마무리 단계에서 sudo 환경 결함(파일시스템 ACL · immutable 비트 · NFS 권한 거부 등) 으로 일부 경로의 소유권/그룹 쓰기 권한을 정상화하지 못한 경우 즉시 콘솔에 실패 경로 + 운영자 수동 복구 명령(`sudo chown -R …` / `sudo chmod -R g+w …`) 안내 — 이전에는 silent fail 로 묻혀 운영자가 후속 권한 거부 발생 후에야 인지하던 문제 해소\n\n#### 알림 시스템\n\n- 권한 기반 수신자 타입 추가 — `permission` 타입으로 특정 권한을 가진 모든 사용자에게 알림 발송 가능 (예: 게시판 신고 알림은 신고 관리 권한자에게 자동 발송)\n- 모듈 환경설정에서 알림이 비활성화된 경우 발송 자체를 사전 차단하는 정책 게이트 도입 — 이전에는 수신자 해석 단계가 정책을 우회하여 발송되던 문제 해소\n- 게시판 환경설정 → 신고 정책 탭에 신고 알림 채널 선택 UI 추가 (이메일 / 사이트 알림 다중 선택)\n- 모듈/플러그인이 자기가 발송하는 알림 정의를 manifest 에서 직접 선언하도록 통일 — 활성화/업데이트 시 운영자 편집값을 보존하면서 자동 등록되고, 제거 시 함께 정리됨. 권한·메뉴·본인인증과 동일한 declarative getter 패턴\n- 코어 기본 알림(회원가입 환영·비밀번호 재설정·비밀번호 변경) 의 다국어 제목/본문/변수/채널 정의를 `config/core.php` 로 통합 — 운영자가 코드 수정 없이도 향후 표면 변경을 추적할 수 있는 단일 소스 확보\n\n#### 본인인증\n\n- 본인인증(IdentityVerification) 인프라 도입 — 코어에 범용 IDV 프로바이더 계약을 마련하고, 기본 메일 프로바이더를 내장. 플러그인이 KCP·이니시스·SMS 등 다른 경로를 동일 계약으로 붙일 수 있도록 확장점 제공\n- 외부 본인인증 provider (KCP·PortOne·토스인증·Stripe Identity 등) 가 G7 표준 Extension Point 패턴으로 자기 SDK UI 를 주입할 수 있는 슬롯 도입\n- 비동기 검증 흐름을 위한 백엔드 폴링/콜백 엔드포인트 추가 — `GET /api/identity/challenges/{id}` (상태 폴링), `POST /api/identity/callback/{providerId}` (외부 redirect 콜백 수신)\n- 본인인증 정책 시스템 신설 — 회원가입·비밀번호 재설정·민감 작업에 적용되는 모든 본인인증 시점을 라우트/훅 단위로 선언형 정책으로 통합 관리. 관리자 화면에서 정책 활성/유예 시간/프로바이더/실패 모드/단계(가입 제출 전 vs 가입 후 활성화 전)·적용 대상(self/admin/both)을 조정할 수 있으며, 운영자 수정값은 업데이트 재시딩 시 보존됨\n- 본인인증 정책의 enable 토글이 라우트 코드 수정 없이 즉시 적용 — 모든 API 라우트가 정책 DB 와 자동 매칭되어 운영자가 admin UI 에서 정책을 켜는 즉시 본인인증이 강제됨. 코어/모듈/플러그인 어떤 라우트의 응답 처리 패턴에서도 본인인증 흐름이 일관되게 모달까지 도달하도록 처리\n- 회원가입 단계 정책 2종 시드 — 가입 제출 전 동기 검증, 가입 후 활성화 전 비동기 challenge (기본값 비활성, 운영자 opt-in)\n- 본인인증 정책이 활성화된 모든 강제 지점(회원가입·비밀번호 재설정·민감 작업·게시판/이커머스 정책 등)에서 사용자/관리자 화면에 동일한 모달 UX 가 자동으로 표시되도록 코어 인터셉터와 공통 모달 인프라 도입\n- 전역 프론트엔드 인터셉트 — 서버가 HTTP 428 본인인증 요구 응답을 반환하면 자동으로 인증 모달을 띄우고 사용자가 인증에 성공하면 원 요청을 자동 재실행\n- 본인인증 가드가 모듈/플러그인이 선언한 훅에도 자동 적용되도록 동적 구독 도입 — 결제 직전·민감 액션 직전 등 도메인 특화 가드를 운영자 토글 한 번으로 활성화\n- 관리자 화면에 본인인증 정책 관리 페이지 추가 (정책 목록 DataGrid + 편집)\n- 관리자 화면에 본인인증 이력 페이지 추가 — 알림 발송 이력과 동일한 수준의 UI 제공: 인증 수단(Provider) 탭(활성화된 프로바이더에 따라 자동 갱신), 통합 검색(자동 감지/사용자 ID/대상 식별자/IP/정책 키), 상태·인증 목적·채널·발생 유형 다중선택 OR 필터, 날짜 범위 + 고급 검색(발생 유형·출처·정책 키) 토글 영역, 정렬·페이지 사이즈 선택, 행 펼침으로 인라인 상세 표시, 모바일 반응형, 사용자 타임존 기준 시각, 보관주기(180일) 일괄 파기, 인증 수단/발생 위치 다국어 라벨 표시\n- 본인인증 목적(purpose) 의 출처(source) 추적 — 코어/모듈/플러그인이 선언한 목적을 구분해 환경설정 화면에서 분리 표시하며, 코어 정책 화면 상단에 \"코어가 제공하는 본인인증 목적\" 칩 섹션(목적 코드/라벨 + 설명·허용 채널 툴팁) 추가\n- 모듈 환경설정의 본인인증 정책 탭을 코어 정책 관리 화면과 동일한 UI/UX 로 통일 — 검색·필터(강제 시점/출처)·페이지네이션·정책 추가/편집 모달 직접 호출(코어 화면으로 이동 X)·이 모듈이 등록한 본인인증 목적 칩 섹션(없으면 안내 메시지). 정책 목록의 \"인증 목적\" 컬럼이 코드명 대신 사람이 읽을 수 있는 라벨로 표시\n- 모듈/플러그인이 자기 컨텍스트의 본인인증 정책·목적·메시지 정의를 manifest 에서 직접 선언하도록 통일 — `module.php::getIdentityPolicies()` / `getIdentityPurposes()` / `getIdentityMessages()` 선언만으로 코어 정책 관리에 자동 연동되며, 활성화/업데이트 시 운영자 편집값을 보존하면서 자동 등록·정리됨. 권한·메뉴·알림과 동일한 declarative getter 패턴\n- 본인인증 정책/이력 목록 API 가 source 컨텍스트별 필터를 받아 모듈 환경설정 탭이 자기 정책/이력만 조회 가능하며, 운영자 정의 정책을 특정 모듈/플러그인 컨텍스트에 귀속시켜 추가할 수 있도록 source_identifier 입력 허용 (`admin` / `module:{id}` / `plugin:{id}`)\n- 본인인증 메일 메시지 템플릿 시스템 도입 — 프로바이더와 (목적/정책)별로 다국어 제목/본문을 개별 정의 가능하며, 메시지 발송 시 정책 → 목적 → 프로바이더 기본값 순서로 fallback 해석. 회원가입/계정 변경/중요 작업/비밀번호 재설정 + 프로바이더 기본값 등 5종 메일 템플릿이 한국어/영어로 시드되어 즉시 발송 가능\n- 본인인증 challenge 발급/검증/취소 라우트에 권한 미들웨어 + scope=self 가드 적용 — 게스트는 `core.identity.{request,verify,cancel}` 권한으로 비로그인 가입(Mode B) 흐름 진입, 로그인 사용자는 본인 challenge 만 다룰 수 있으며 관리자는 임의 challenge 도 처리 가능. 모달 취소 시 서버 cancel API 를 호출해 challenge 가 audit log 에 cancelled 상태로 즉시 기록되도록 정합화\n- 환경설정 → 본인인증 탭에 \"메시지 템플릿\" 서브탭 신설 — 알림 템플릿 관리와 동일한 UX (채널 서브탭·페이지당 항목 수 셀렉터·카드 펼침 본문 미리보기·활성/기본 배지·페이지네이션) 로 정의 목록·활성 토글·다국어 편집(변수 가이드 + 기본값 복원) 제공. 운영자가 추가한 정책에 전용 메일 메시지 정의를 화면에서 직접 등록·삭제 가능 (시드 기본값 보호 + 정책 키 매칭 검증). 외부 본인인증 프로바이더 플러그인이 자기 메시지 기본값을 코어 복원 로직에 기여할 수 있는 필터 훅 노출\n- 알림 발송 이력 / 본인인증 이력 샘플 시더를 코어/모듈별로 분리 — 코어 시더는 코어 정의·정책만, 각 모듈 시더는 자기 영역만 채우도록 영역 격리. 모듈은 `module:seed {id} --sample` 으로 자기 영역 이력만 독립 생성 가능. 샘플 데이터는 실제 등록된 사용자·정의·정책·프로바이더 기반으로 생성되어 운영 데이터와 동일한 스키마/분포 유지\n- 모듈/플러그인 제거 시 코어 공유 테이블에 적재된 해당 확장의 데이터(권한·관리자 메뉴·알림 정의·본인인증 정책·본인인증 메시지 정의·본인인증 목적) 가 함께 정리되며, 제거 모달의 \"삭제될 데이터\" 에도 항목별 건수가 표시되도록 개선\n- IDV 도메인 분류 데이터(목적·채널·트리거 출처·정책 범위·정책 실패 모드·정책 적용 대상·정책 출처·메시지 스코프) 8종을 PHP Backed Enum 으로 정의하여 정책 등록/수정 시 일관된 검증 적용\n\n#### 인스톨러\n\n- 인스톨러 설치 완료 화면에 설치된 코어 버전 표시 — 사용자가 어떤 버전이 설치되었는지 즉시 확인할 수 있도록 개선\n\n#### 언어팩 시스템\n\n- 새 언어(일본어/중국어 등)를 코어 수정 없이 추가할 수 있는 언어팩(Language Pack) 시스템 도입\n- ZIP 업로드 또는 GitHub URL로 언어팩 설치/제거/활성화 지원\n- 동일 언어 슬롯에 여러 벤더의 언어팩 공존 가능 — 라디오 전환으로 즉시 활성 변경\n- 코어/모듈/플러그인/템플릿 별도 적용 — 모듈 언어팩은 해당 코어 언어팩이 활성일 때만 활성화 가능\n- 관리자 메뉴: 환경설정 > 언어팩 관리(통합) + 모듈/플러그인/템플릿별 진입점 추가\n- 사용자가 직접 수정한 다국어 키는 언어팩이 덮어쓰지 않도록 보존 정책 적용\n- 보안: 언어 번역 외의 PHP 실행 코드 포함 시 설치 차단\n- 언어팩 관리 화면을 모듈 관리와 동일 수준의 운영 도구로 보강 — 검색(식별자/벤더/언어), 업데이트 확인, 캐시 갱신, 다중 선택 일괄 제거 지원\n- 활성/비활성 상태를 토글 스위치로 일원화 (보호된 팩은 비활성화된 토글로 표시)\n- 업데이트 가능 항목에 \"업데이트 가능\" 배지와 행 단위 업데이트 실행 버튼 노출\n- 정보 모달을 모듈 정보 모달과 동일한 4섹션 구조(기본정보 / 호환성 / 소스 정보 / 변경로그)로 제공하며, CHANGELOG.md 를 자동 파싱하여 버전별 카테고리로 표시\n- 설치 모달에서 ZIP 업로드 전에 manifest 와 검증 결과를 사전 확인할 수 있는 미리보기 제공\n- 모듈/플러그인/템플릿별 언어팩 페이지 진입 시 대상 확장 안내 배너와 환경설정 탭으로 회귀하는 링크 노출\n- 언어팩 업데이트 권한을 별도 권한 키로 분리하여 설치/관리 권한과 독립적으로 부여 가능\n- 언어팩 운영 풀 패리티 — 모듈/플러그인/템플릿 관리와 동일한 안전 장치/확장성 적용\n- 공식 일본어(ja) 번들 언어팩 12종 추가 — 코어, 주요 모듈(전자상거래/게시판/페이지), 주요 플러그인(CKEditor5/마케팅/토스페이먼츠), 기본 템플릿(admin/user)을 일본어로 즉시 사용 가능\n- 언어팩 관리 화면에서 번들 언어팩을 모듈/플러그인 관리와 동일하게 \"미설치\" 상태로 노출 — 행별 \"설치\" 버튼으로 즉시 설치 가능\n- 언어팩 목록 필터에 \"미설치 (번들)\" 상태 옵션 추가\n- 본인인증 메일 메시지 정의의 다국어 키를 언어팩으로 주입할 수 있도록 확장 — 코어 수정 없이 언어팩 ZIP 만으로 IDV 메일 본문/제목을 다국어화 가능\n- 모듈/플러그인이 선언한 알림/본인인증 메시지 정의의 다국어 키도 언어팩으로 주입되도록 정합 — 이전에는 hook 발화 누락으로 코어 정의에만 적용되던 동작 정상화\n - 업데이트 시 자동 백업 + 실패 시 직전 버전으로 자동 복구\n - 동시성 가드 — 진행 중인 작업 동안 활성/비활성/제거/재업데이트 진입 차단\n - 번들 디렉토리(`lang-packs/_bundled/{identifier}`) 에서 외부 다운로드 없이 (재)설치하는 경로 추가 — 코어 언어팩 복구/재배포 단순화\n - 라이프사이클 훅 명명을 모듈/플러그인/템플릿과 통일 — `core.language_packs.{installed|updated|uninstalled|activated|deactivated}` 발행 (확장 가능성 확대)\n - Artisan 커맨드 신규 — `language-pack:list`, `language-pack:install`, `language-pack:update`, `language-pack:uninstall`\n- 코어와 번들 확장(모듈/플러그인/템플릿)에 내장된 한국어/영어를 가상 보호 언어팩으로 자동 노출 — 별도 설치 없이 언어팩 관리자에서 항상 활성/보호 상태로 확인 가능, 수정/제거 차단\n- 호스트 확장 비활성화 시 그에 종속된 언어팩이 함께 비활성화되며, 재활성화 시 \"다음 언어팩도 활성화하시겠습니까\" 모달로 사용자 의사 확인 후 일괄 활성화\n- 여러 언어팩을 한 번에 활성화하는 `POST /api/admin/language-packs/bulk-activate` API 추가 (의존성/버전 호환성 자동 검사)\n- 언어팩 활성화 시 의존성 + 호스트 확장 버전 호환성 검사 자동 수행 — 호스트 확장이 비활성/미설치이거나 버전 미달이면 활성화 차단\n- 언어팩 업데이트 우선순위를 모듈/플러그인 패턴으로 정합화 — GitHub 1순위 + bundled 폴백, 강제 업데이트 시 bundled 우선\n- 언어팩 상세 모달에서 코어/번들 확장 내장 항목 클릭 시 \"별도 언어팩이 아닌 내장 번역\" 안내 배너 노출\n- 모듈/플러그인/템플릿 상세 모달의 닫기 버튼이 콘텐츠 길이와 무관하게 모달 하단에 항상 보이도록 sticky 처리\n\n#### SEO\n\n- 관리자 환경설정 > SEO 탭의 Sitemap 카드에서 sitemap 을 즉시 재생성하고 마지막 생성 시각을 확인할 수 있는 \"지금 생성\" 버튼 추가 — 큐 드라이버와 무관하게 동기 실행되며 결과를 즉시 표시\n\n#### SEO 봇 감지\n\n- 봇 감지 엔진을 `jaybizzle/crawler-detect` 라이브러리로 교체. 기본 약 1,000종의 봇(검색엔진·링크 미리보기·AI 검색 등)이 자동 감지됨 — 링크를 슬랙·페이스북·LinkedIn·트위터·디스코드·텔레그램 등에 붙여넣으면 제목·설명·이미지 미리보기가 즉시 동작\n- 라이브러리가 놓치는 봇 3종(`kakaotalk-scrap`·`Meta-ExternalAgent`·`ChatGPT-User`) 을 G7 보강 패턴으로 기본 포함\n- 봇 감지 확장 훅 `core.seo.resolve_is_bot` 신설 — 플러그인이 IP 범위 검증·역방향 DNS·Cloudflare 봇 점수 등을 주입할 수 있는 슬롯\n- \"봇 라이브러리 사용\" 관리자 토글 추가 (기본 on, 비활성 시 운영자 커스텀 목록만 사용하는 레거시 모드)\n\n#### SEO OG / Twitter 카드 / 도메인 ownership\n\n- 슬랙·페이스북 링크 미리보기에 이미지·카드가 표시되도록 OG 보강 태그를 자동 출력하도록 개선 — og:site_name, og:image:width, og:image:height, og:image:secure_url, og:image:type, og:image:alt, og:locale 추가\n- Twitter 카드 메타태그(twitter:card / twitter:site / twitter:title / twitter:image 등) 출력 신설 — 슬랙 unfurl 폴백 경로 정상화\n- 운영자 환경설정 SEO 탭에 \"OG / Twitter 카드 기본값\" 카드 추가 — 사이트 이름·이미지 기본 가로/세로·Twitter 카드 타입·Twitter 사이트 핸들 5개 입력\n- 모듈·플러그인이 자기 도메인의 OG/Twitter/JSON-LD 를 직접 선언하도록 SEO declaration API 신설 — 이커머스 상품(Product/Offer/AggregateRating)·게시판 게시글(Article) 등 도메인 스키마가 레이아웃에서 확장 코드로 owned 되어 데이터에 따라 정확한 부속 태그 생성\n- SEO 메타 확장 훅을 분기별·통합 모두 제공 — OG·Twitter·구조화 데이터 각각의 hook 슬롯과 통합 hook 모두 지원하여 확장이 원하는 단계에서 선택 변경 가능\n- 모듈 설정 타이틀 템플릿에서 변수가 비어있을 때 인접 구분자(- – · |) 가 자동 정리되도록 개선 — 옵셔널 그룹 `[ ... ]` 표기도 지원하여 페이지별 구성 명시 가능\n\n### Changed\n\n- 콘솔 confirm 입력 처리 통일 — yes/y, no/n 외 입력 시 안내 메시지 출력 후 재질문, empty 입력 시 default 사용. 코어 업데이트·매니저 커맨드(module/plugin/template install·update·uninstall)·설정 마이그레이션의 모든 yes/no 프롬프트에 동일 규칙 적용 (#15 @laelbe 님께서 건의해주셨습니다.)\n- 비밀번호 재설정 정책 기본값을 비활성으로 변경 — 본인인증 인프라가 미구성된 사이트에서도 기본 동작이 영향받지 않도록 운영자 opt-in 으로 전환\n- 본인인증 정책의 인증 조건(`conditions`) 운영자 편집 허용 — 회원가입 단계 등 정책 조건을 코드 수정 없이 관리자 화면에서 조정 가능. 모듈 업데이트 시 운영자 수정값 보존\n- 토큰 만료 등으로 권한 없는 레이아웃 진입 시 \"페이지 로딩 실패\" 에러 화면 대신 로그인 페이지로 자동 이동 — 로그인 화면에서 \"세션이 만료되었습니다. 다시 로그인해 주세요.\" 토스트로 사용자에게 안내. 템플릿이 자체 로그인 경로를 사용하는 경우 부트스트랩에서 인증 설정을 커스터마이즈할 수 있는 공개 API 도 함께 제공 (#19 @abc101 님께서 건의해주셨습니다.)\n- 템플릿 다국어 데이터 로딩 시 활성 언어팩의 다국어가 가장 높은 우선순위로 병합되도록 변경\n- 권한·역할·메뉴·알림 등 코어 기본 데이터와 배송유형·클레임 사유·게시판 유형 등 모듈 기본 데이터에 활성 언어팩의 다국어가 자동 반영되도록 개선\n- 사용자 수정 보존(user_overrides) 정책을 다국어 JSON 컬럼은 sub-key 단위(`name.ko` 등) 로 기록하도록 개선 — 운영자가 한 언어 라벨만 수정해도 그 언어만 보존되며, 신규 활성 언어팩(예: 일본어 추가) 의 라벨은 자동 동기화됨. 기존 컬럼 단위(`name`) 기록은 업그레이드 시 활성 locale dot-path 로 자동 변환\n- 언어팩 활성/비활성 시점에 영향받는 모듈/플러그인의 entity 시더가 자동 재실행되어 신규 언어 라벨이 즉시 DB 에 반영되도록 라이프사이클 통합 — scope 별 라우팅 (코어 언어팩 → 모든 활성 확장, 모듈 언어팩 → 해당 모듈만, 플러그인 언어팩 → 해당 플러그인만)\n- 환경설정 → SEO → \"추가 봇 패턴\" 필드의 역할 변경 — 기존에는 유일한 봇 매칭 소스(기본 5종)였으나, 이제는 라이브러리가 놓치는 조직별 커스텀 봇만 추가하는 보강 레이어로 동작. 기존 설치의 운영자 커스텀 값은 모두 보존되며, 신규 설치 기본값은 jaybizzle 미커버 3종으로 변경\n\n### Security\n\n- 회원가입·비밀번호 재설정 라우트에 본인인증 정책 강제 미들웨어 부착 — 정책이 활성화된 경우 미인증 요청을 라우트 단계에서 차단 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)\n- 플러그인이 정책 해석 필터 훅에서 잘못된 타입을 반환해도 원본 정책이 유지되도록 우회 차단 강화 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)\n- 웹 인스톨러의 Composer/PHP 바이너리 경로 검증에서 사용자 입력이 그대로 shell 명령으로 실행될 수 있던 문제 수정 — 입력은 실행 가능한 단일 파일 경로로만 허용하고 모든 분기에서 인자 escape 강제. 설치 워커가 동일한 입력을 사용하던 내부 헬퍼도 같은 정책으로 정렬 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)\n- 설치 단계 4 의 확장 기능 선택 API 가 사용자가 보낸 모듈/플러그인/템플릿/언어팩 식별자에 셸 메타문자 검증을 적용하도록 강화 — 부적절한 식별자는 400 응답으로 거부되어 이후 설치 명령에 도달하지 않음 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)\n- 인스톨러의 코어 업데이트 _pending 경로 검증이 `..` 등 부모 디렉토리 우회 시도를 거부하고 응답 메시지를 단일화하여 임의 디렉토리 enumeration 신호 차단 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)\n- 설치 시 `.env` 작성 헬퍼가 입력값에 포함된 개행 문자를 제거하도록 변경 — DB 비밀번호 등 사용자 입력으로 새로운 환경 변수 라인이 주입되는 시나리오 차단 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)\n- 데이터베이스 연결 정보의 host/port/database 값에 DSN 키-밸류 구분자(`;`, `=`) 또는 NUL/CRLF 가 포함되면 연결을 거부하도록 추가 검증 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)\n- 설치가 완료된 시스템에서 `public/install/` 하위 모든 엔드포인트가 비즈니스 로직 진입 전 HTTP 410 으로 차단되도록 공통 가드 도입 — 운영 환경에서 인스톨러 노출형 결함의 공격 표면 제거. 운영자가 인스톨러를 다시 사용해야 하는 경우 설치 완료 마커(`storage/app/g7_installed`) 와 `.env` 의 `INSTALLER_COMPLETED` 를 모두 제거 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)\n\n### Fixed\n\n- 코어 업데이트 후 일부 환경에서 모듈/플러그인이 저장한 사용자 데이터(상품 이미지·첨부파일 등)에 PHP 가 접근하지 못해 \"찾을 수 없음\" 오류가 발생하던 문제 수정 — 업데이트 종료 시점 권한 복원 범위를 PHP 쓰기 영역(storage/logs·storage/framework·bootstrap/cache·storage/app/core_pending)으로 한정하고 항목별 정확 복원으로 정합화. 이전 버전에서 본 릴리즈로 업그레이드한 환경에서는 업그레이드 시점에 storage/app 디렉토리/파일 권한을 PHP 가 접근 가능한 형태로 자동 정상화함. 향후 업데이트에서는 사용자 데이터 디렉토리에 자동 생성되는 보존 마커로 권한이 영구 보호됨\n- PHP 8.5 환경에서 모든 페이지 응답이 손상되어 Firefox 에서는 \"Content Encoding Error\", Edge/Chromium 에서는 빈 화면으로 표시되던 문제 수정\n- `php artisan serve` 환경에서 인스톨러 진행 중 .env 파일이 변경되면 개발 서버가 워커를 재시작하면서 설치 단계가 중도에 끊기던 문제 수정 — 설치 진행 중에는 .env 를 건드리지 않고 완료 화면 노출 후 한 번에 반영하도록 변경\n- 관리자 환경설정 \"시스템 사양\" 카드의 메모리 항목이 서버 물리 메모리가 아닌 현재 PHP 프로세스가 사용 중인 메모리(수 MB 단위)로 표시되던 문제 수정 — 디스크 사용량과 동일한 형식(사용량/전체/백분율)으로 실제 서버 RAM 을 표시하도록 개선\n- 관리자 환경설정 \"시스템 사양\" 카드의 CPU 항목이 Windows 11 / Windows Server 2025 에서 \"operable program or batch file.\" 로 표시되던 문제 수정 — 해당 OS 에서 제거된 wmic 의존을 걷어내고 PowerShell 기반 조회로 전환, 구형 Windows 환경에서는 기존 방식으로 자동 폴백\n- 관리자 환경설정 \"정보\" 탭을 한 번 조회한 뒤 다른 탭으로 전환할 때마다 수 초 지연이 반복되던 문제 수정 — 시스템 정보 조회 결과를 1시간 캐싱하여 탭 전환 시 대기 시간 제거 (시스템 캐시 초기화 시 함께 무효화)\n- 큐 워커가 훅 페이로드의 enum 값(주문 상태 등)을 복원하지 못해 활동 로그·알림 등 일부 후속 처리가 실패하던 문제 수정\n- 항목 삭제 후 큐 워커가 처리하는 후속 훅에서 대상 데이터를 복원하지 못해 처리가 중단되던 문제 수정 — 소프트 삭제 페이지·첨부파일, 하드 삭제 주문 옵션 등 포함\n- 페이지 키워드 검색 결과의 전체 건수가 발행된 모든 페이지 수로 부풀려지던 문제 수정\n- 사용자가 댓글을 단 게시글 활동 조회 시 500 에러가 발생하던 문제 수정\n- 사용자 활동 통계에 삭제된 게시글의 댓글 수가 포함되던 문제 수정\n- 일부 주문에서 옵션 정보 직렬화 시 500 에러가 발생하던 문제 수정\n- 상품 옵션 삭제 검증 시 런타임 에러가 발생하던 문제 수정\n- 설치 직후 또는 활성 모듈 디렉토리 부재 시 이커머스 환경설정 기본값이 비어있던 문제 수정\n- 검색 가능 드롭다운(SearchableDropdown)에서 빠른 모달 전환 시 race condition 가능성 차단\n- 반응형 설정과 반복 렌더링이 결합된 레이아웃에서 무한 재귀가 발생할 수 있던 엔진 결함 수정 (engine-v1.43.1)\n- 슬롯 치환 시 텍스트 노드를 컴포넌트로 가정하여 발생할 수 있던 레이아웃 렌더링 오류 차단\n- 인스톨러 실행 시 보안 키 생성 단계에서 개발용 패키지 ServiceProvider 를 찾지 못해 설치가 중단되던 문제 수정 — 이전 환경에서 남은 컴파일 캐시를 vendor 교체 직후와 보안 키 생성 직전에 자동 정리하도록 개선\n- 폴링 모드 인스톨러가 큰 확장(레이아웃·테스트 수천 파일) 설치 도중 멈추던 문제 수정 — 명령 출력을 파일 기반으로 처리하여 OS 파이프 버퍼 한도와 무관하게 동작\n- PHP 8.5 + Apache + mod_fcgid 환경에서 폴링 모드 진행 상황이 실시간 반영되지 않고 설치 완료 시점에 일괄 표시되던 문제 수정 — 어느 모드로도 진행 상황이 즉시 표시되도록 보정. Apache 환경별 권장 설정은 INSTALL.md 와 시스템 요구사항 문서에 명시\n- 설치 완료 후 임시 파일이 자동 정리되지 않던 문제 수정\n- 알림 템플릿 편집 모달의 입력 필드에서 글자를 입력할 때마다 화면이 심하게 버벅이던 문제 수정\n- 슈퍼관리자가 다른 관리자 계정을 삭제할 수 없던 문제 수정 — 관리자 계정 삭제 가능 여부를 역할/권한/스코프 설정에 따라 판단하도록 개선 (슈퍼관리자 본인 보호는 유지)\n- `seo:generate-sitemap` 커맨드가 큐 드라이버 설정과 무관하게 항상 \"큐에 디스패치\" 안내를 출력하던 문제 수정 — 동기 드라이버에서는 즉시 생성으로 동작하고 그에 맞는 안내를 표시하도록 변경\n- 관리자 템플릿을 활성화해도 즉시 반영되지 않아 사용자가 직접 새로고침해야 하던 문제 수정 — 관리자 템플릿 활성화 시 자동으로 페이지를 갱신하여 새 템플릿이 즉시 적용되도록 개선\n- 보안 환경설정의 \"최대 로그인 시도 횟수 / 차단 시간\" 설정이 실제로 적용되지 않아 무제한 로그인 시도가 가능하던 문제 수정 — 임계 도달 시 계정 잠금(HTTP 423), 잠금 해제 시각 안내 토스트, per-IP 백업 throttle, 활동 로그 기록까지 통합 구현\n- 게시판 글쓰기 화면을 URL 로 직접 진입하거나 강제 새로고침했을 때 업로드한 첨부파일이 게시글에 연결되지 않던 문제 수정 (engine-v1.49.2) (#24 @minyho 님께서 제보해주셨습니다.)\n- 코어 업그레이드 후 새 버전에서 추가된 권한·메뉴·알림 정의가 등록되지 않아 관리자 화면에서 \"해당 권한이 없습니다\" 가 반복 표시되거나 신규 메일 템플릿이 비어있던 문제 수정 — 업그레이드 시 새 버전 설정 파일을 정확히 인식하도록 보정. 본 릴리즈로 업그레이드하면 누락분이 자동 등록됨\n- PHP 8.5 환경에서 관리자 로그인 시 `PDO::MYSQL_ATTR_SSL_CA` 등 PDO 드라이버 상수가 deprecation 경고를 발생시키던 문제 수정 — `Pdo\\Mysql::ATTR_SSL_CA` 형태의 신규 상수로 전환하고, PHP 8.5 미만 환경에서는 기존 상수를 그대로 사용하도록 분기 처리 (#23 @yks118 님께서 제보해주셨습니다.)\n- 회원가입 시 일부 환경에서 동의 항목 메타데이터가 누락되어 활동 로그 처리에서 예외가 발생, 후속 훅 체인이 중단되며 신규 회원에게 `user` 역할이 자동 부여되지 않던 문제 수정 — 동의 메타데이터를 nullable 로 받아 누락 케이스에서도 후속 권한 부여가 정상 동작하도록 개선 (#25 @comtylove-netizen 님께서 제보해주셨습니다.)\n\n## [7.0.0-beta.3] - 2026-04-23\n\n### Fixed\n\n- CKEditor5 플러그인 활성 상태에서 게시판 글쓰기 저장 시 \"제목은 필수입니다\" 422 오류가 발생하던 호환성 문제 수정 — 제목·내용 입력 순서와 무관하게 정상 저장되도록 개선 (#17 @laelbe 님께서 제보해주셨습니다.)\n- 코어 업데이트가 sudo 로 실행된 환경에서 캐시·세션·확장 디렉토리의 그룹 쓰기 권한이 일부 손실되어 업데이트 직후 \"Permission denied\" 또는 플러그인 제거 검증 실패가 발생하던 문제 수정 — 업데이트 종료 시점에 그룹 쓰기 권한을 자동 정상화하며 기존 손실 분은 1회성 복구 스텝으로 회수\n- 업데이트 완료 후 Laravel 런타임이 새로 만드는 캐시·세션 하위 디렉토리가 기본 umask(022) 때문에 다시 그룹 쓰기 권한을 잃어 재차 \"Permission denied\" 가 발생하던 문제 수정 — 업그레이드 스텝이 업데이트 진행 프로세스의 umask 를 그룹 쓰기 친화적으로 전환하고, 이후 부팅 시점에도 `storage/` 의 현재 그룹 쓰기 설정을 감지해 프로세스 umask 를 자동 동조 (운영자가 그룹 공유를 비활성화한 환경은 그대로 보존)\n- 코어 업데이트 마지막 단계의 진행 표시줄이 끝난 뒤 줄바꿈 없이 다음 셸 프롬프트가 같은 줄에 붙어 표시되던 출력 문제 수정\n\n### Notes\n\n- 7.0.0-beta.1 에서 7.0.0-beta.3 로 직접 업그레이드하는 경우, 환경(opcache CLI 활성 등)에 따라 권한 복구 스텝이 자동 실행되지 않고 건너뛰어질 수 있습니다. 업데이트 후 `storage/framework/cache` 등에서 Permission denied 가 발생하면 아래 명령을 수동 실행해 주세요 — 7.0.0-beta.2 에서 올라오는 경로에서는 해당 없음\n - `php artisan core:execute-upgrade-steps --from=7.0.0-beta.2 --to=7.0.0-beta.3 --force`\n- 시스템 레벨에서도 일관된 그룹 공유 권한을 원하면 php-fpm pool 설정에 `umask = 002` (또는 systemd unit 의 `UMask=0002`) 추가를 권장합니다. 코드 레벨 동조와 병행하면 외부 프로세스(cron, composer 등) 도 동일 권한으로 파일을 생성합니다\n\n## [7.0.0-beta.2] - 2026-04-20\n\n### Added\n\n#### 알림 시스템 재설계\n\n- 알림 시스템을 Definition(정의) × Template(채널별 템플릿) × Recipients(수신자) 3계층으로 재설계\n- 채널별 독립 수신자 설정 — trigger_user, related_user, role, specific_users 4종 타입 지원\n- 채널 Readiness 검증 — 미설정 채널(SMTP 미구성 등)의 발송을 사전 차단\n- 알림 발송 공통 디스패처 — 채널별 독립 발송, 발송 전후 훅 자동 실행, 모든 채널 자동 로깅\n- 알림 클릭 URL 지원 — database 채널 알림에 클릭 시 이동할 URL 패턴 설정 가능\n- 사용자/관리자 알림 전체 삭제 API 추가\n- 비밀번호 변경 시 `password_changed` 알림 자동 발송\n- 코어 권한에 사용자 알림 전용 카테고리 분리 (관리자/사용자 권한 의미 분리)\n- 알림 정의 일괄 초기화 기능 — 커스터마이징된 알림의 모든 채널 템플릿을 기본값으로 복원 (확인 모달 + 스피너 UX 포함)\n- 모듈/플러그인이 자신의 기본 알림 정의를 코어 리셋 로직에 제공할 수 있는 필터 훅 제공\n- 템플릿 편집 시 편집 상태가 자동 추적되어 리셋 버튼이 자동 노출\n- 알림 정의/템플릿 응답에 편집/삭제 권한 정보(abilities) 포함으로 UI가 권한에 따라 자동 조정\n\n#### Vendor 번들 시스템\n\n- Composer 실행 불가 환경(공유 호스팅 등)을 위한 vendor 번들 시스템 추가\n- vendor-bundle Artisan 커맨드 — 빌드, 검증, 일괄 처리\n- 모듈/플러그인 설치·업데이트에 `--vendor-mode=auto|composer|bundled` 옵션 추가\n- 인스톨러 Step 3에 Vendor 설치 방식 선택 UI 추가 (환경 자동 감지)\n\n#### GeoIP 및 타임존\n\n- MaxMind GeoLite2 자동 다운로드 스케줄러 + 관리자 환경설정 UI 추가\n- IANA 타임존 전체(약 425개) 지원 — 기존 7개 하드코딩 화이트리스트 폐기 (#8 @abc101 님께서 건의해주셨습니다.)\n- Select 컴포넌트에 `searchable` prop 추가 — 타임존 등 대량 옵션에서 검색 가능\n\n#### 인스톨러 개선\n\n- SSE/폴링 듀얼 모드 지원 — Nginx 프록시 + Apache 환경에서 SSE 문제 시 폴링 모드로 전환 가능\n- 확장 의존성 자동 해결 — 템플릿 선택 시 필요한 모듈/플러그인을 즉시 자동 선택, 전이적 의존성까지 해결 (#10 @glitter-gim 님께서 건의해주셨습니다.)\n- 자동 선택된 항목을 시각적으로 구분하고 요구한 확장 이름을 함께 표시\n- 다른 확장이 의존하는 항목은 선택 해제 차단 (의존 관계 안내 메시지 포함)\n- 의존성 버전 제약 사전 검증 — 버전 불일치 시 설치 진행 전 경고 (semver 비교: `>=`, `^`, `~` 등 지원) (#3 @laelbe 님께서 제보해주셨습니다.)\n- 기존 DB 테이블 감지 및 안전한 재설치 지원 — 백업 안내 + 명시적 동의 후 진행 (#5 @laelbe 님께서 건의해주셨습니다.)\n- 권한 안내 단순화 — `chmod -R 755` 단일 명령어로 통합 (업계 표준 정렬)\n- 소유자 불일치(`ownership_mismatch`) 감지 — 전통적 Apache 환경에서 3가지 해결 옵션 제시\n- Step 5에 \"설치 시작\" 버튼 도입 — 모드 선택 후 사용자 클릭으로 설치 시작\n\n#### 공통 캐시 시스템\n\n- 코어/모듈/플러그인 격리 캐시 시스템 도입 (접두사 자동 부여)\n- 모델 변경 시 태그 기반 자동 캐시 무효화 트레이트 추가\n- 환경설정에서 캐시 TTL 중앙 관리 (7개 설정 키)\n\n#### 어드민 UI\n\n- 페이지 진입/탭 전환 시 로딩 spinner 표시 — 데이터 fetch 완료까지 유지 (#6 @laelbe 님께서 제보해주셨습니다.)\n- 목록 페이지네이션 시 DataGrid body 영역 한정 spinner (pagination 버튼 가림 방지)\n- DataGrid 컴포넌트에 `id` prop 추가\n\n#### 기타\n\n- HookManager 공통 큐 디스패치 — 환경설정 하나로 모든 훅 리스너 자동 비동기 실행, 큐 워커에서도 Auth/Request/Locale 컨텍스트 자동 복원\n- WebSocket 클라이언트/서버 endpoint 분리 설정 지원 — 리버스 프록시 환경 대응, SSL 검증 옵션 추가\n- SEO 변수 시스템 — 모듈/플러그인이 페이지별 SEO 변수를 제공할 수 있도록 개선\n- 코어 드라이버 확장 시스템 — 플러그인이 필터 훅으로 스토리지/캐시/세션/큐 등 드라이버를 등록 가능\n- `HasUserOverrides` 트레이트 — 시더 데이터와 사용자 수정을 자동으로 분리 추적\n- 템플릿 엔진 engine-v1.42.0 — `render: false` 선택적 리렌더 제어\n- 템플릿 엔진 engine-v1.41.0 — setLocal/dispatch debounce API + stale 값 오염 방지\n- 템플릿 엔진 engine-v1.40.0 — navigate fallback 옵션 (교차 템플릿 경로 대응)\n- 템플릿 엔진 engine-v1.30~38 — sortable wrapper 요소 지정, React.memo 자동 래핑, resolvedProps 참조 안정화, transition_overlay spinner 시스템, reloadExtensions 통합 핸들러\n\n#### 확장 업데이트\n\n- 코어 업데이트 완료 후 `_bundled` 에 새 버전이 있는 확장을 감지하고 일괄 업데이트 여부를 묻는 인터랙티브 프롬프트 제공\n- 일괄 업데이트 시 전역 레이아웃 전략(overwrite / keep) 1회 질의 후 확장별 예외 지정 가능\n- 모듈/플러그인 업데이트에도 `--layout-strategy=overwrite|keep` CLI 옵션 및 관리자 UI 모달 선택 지원 (기존 템플릿 전용 → 확장 전체 일관)\n- 관리자 모듈/플러그인 업데이트 모달에 사용자가 수정한 레이아웃 감지 및 목록 표시 기능 추가\n- 모듈/플러그인 업데이트 시 각 upgrade step 버전을 콘솔에 실시간 출력 (기존에는 파일 로그에만 기록)\n- 확장 업데이트 메서드 파라미터 순서를 3종(템플릿·모듈·플러그인) 공통 prefix(id, force, onProgress) 로 정렬해 일관성 확보\n- 모듈/플러그인이 런타임에 동적으로 생성한 권한·역할·메뉴(예: 게시판별 권한 세트)도 업데이트 시 보존되도록 개선\n- 모듈/플러그인/템플릿 업데이트 커맨드에 업데이트 소스 선택 옵션(`--source=auto|bundled|github`) 추가 — GitHub 원격 장애·태그 롤백 등 상황에서 번들 소스로 강제 설치 가능\n- GitHub 원격에 존재하지 않는 태그로 업데이트 시 \"아카이브 추출 실패\" 로 불명확한 오류가 나던 문제 개선 — 태그 존재 여부를 먼저 검증해 \"업데이트 소스 없음\" 안내로 명확화\n- 관리자 모듈/플러그인 업데이트 모달의 _global 상태키를 `hasModifiedTemplateLayouts`, `hasModifiedModuleLayouts`, `hasModifiedPluginLayouts` 로 통일해 네이밍 충돌 방지\n- `module:install` / `plugin:install` / `template:install` 에 `--force` 옵션 추가 — 이미 설치된 확장도 `_bundled`/`_pending` 원본으로 활성 디렉토리를 덮어써서 재설치 (불완전 설치 복구용)\n- 확장 활성 디렉토리 무결성 검사 추가 — `module.php`/`module.json` (모듈), `plugin.php`/`plugin.json` (플러그인), `template.json` (템플릿) 중 하나라도 누락된 활성 디렉토리를 로드 시 경고 로그 + `install --force` 복구 힌트 제공\n- 코어 업데이트 커맨드에 외부 ZIP 파일 직접 지정 옵션(`--zip=/path/to/g7.zip`) 추가 — GitHub 다운로드 대신 지정 ZIP 을 사용해 오프라인/수동 배포 가능\n- 모듈/플러그인/템플릿 업데이트 커맨드에 외부 ZIP 파일 직접 지정 옵션(`--zip=/path/to/ext.zip`) 추가 — manifest 식별자와 대상 확장이 일치하는지 검증 후 manifest 의 버전으로 업데이트\n- 외부 ZIP 은 GitHub 릴리스 zipball 의 래퍼 디렉토리와 평탄 루트 구조를 모두 자동 감지해 지원\n\n### Changed\n\n#### 알림 시스템\n\n- MailTemplate 시스템을 NotificationDefinition + NotificationTemplate 체계로 전면 전환\n- 코어 알림 3종의 직접 발송을 훅 기반 발송으로 전환\n- 알림 수신자를 definition 레벨에서 template 레벨로 이동 (채널별 독립 수신자)\n- 알림 broadcast 채널을 정수 User ID에서 UUID 기반으로 변경 (보안 강화)\n- 알림 시각 표시를 ISO 8601에서 사용자 타임존 기준으로 변경\n\n#### 캐시 시스템\n\n- 24개 서비스의 캐시 호출을 `Cache::` 파사드에서 `CacheInterface` DI로 전환\n- 13개 서비스의 TTL을 환경설정 중앙 관리로 통일 (하드코딩 제거)\n- 캐시 키 접두사를 `g7:core:`, `g7:module.{id}:`, `g7:plugin.{id}:` 체계로 통일\n- `CacheService` 클래스 삭제 — `CacheInterface`로 완전 대체\n\n#### 인스톨러\n\n- 권한 검증을 비트 값 비교에서 실제 읽기/쓰기 가능 여부 기반으로 단순화\n- `.env` 생성 안내를 소유자 일치 여부에 따라 1-step으로 통합\n\n#### 기타\n\n- 확장 정보 모달에 의존성 정보 노출 — 코어 요구 버전(`g7_version`), 의존 모듈/플러그인 목록과 각각의 요구 버전·설치 버전·충족 상태 뱃지를 모듈/플러그인/템플릿 정보 모달에서 확인 가능\n- `locale_names` 설정 추가 — 언어 표시명을 중앙에서 관리\n- 매 요청 `Schema::hasTable()` 호출 제거 — 설치 완료 환경에서 10~14회의 불필요한 스키마 쿼리 제거로 응답 시간 단축\n- SeoRenderer에 모듈/플러그인 SEO 변수 자동 해석 기능 추가\n- ResponseHelper — 디버그 모드에서 예외 상세 자동 포함, 프로덕션에서 내부 메시지 노출 차단\n- 환경설정 드라이버 변경 시 큐 워커 자동 재시작\n- `allow_url_fopen=Off` 환경 지원 — GitHub 연동 HTTP 호출을 Laravel Http 파사드로 전면 교체\n- 코어 업데이트 완료 후 안내 메시지를 \"수동 업데이트 명령 나열\"에서 \"일괄 업데이트 인터랙티브 프롬프트\"로 변경\n- 확장 삭제 시 권한·메뉴·역할은 \"데이터도 함께 삭제\" 옵션을 체크했을 때만 삭제되도록 변경 — 기본 삭제에서는 보존되어 재설치 시 사용자 역할 할당이 복원\n\n### Fixed\n\n- WebSocket 데이터소스가 progressive 목록에 포함되어 영구 블러되던 문제 수정\n- WebSocket 채널/이벤트 표현식 미평가로 인증 실패하던 문제 수정\n- WebSocket broadcast HTTP API가 클라이언트 host로 POST되어 실패하던 문제 수정\n- WebSocket 활성화 시 필수 필드 미검증 문제 수정\n- WebSocket 비활성화 시에도 Reverb 연결을 시도하던 문제 수정\n- `--force` 업데이트 시 번들 없는 외부 확장이 업데이트 소스를 찾지 못하던 문제 수정\n- 확장 의존성 데이터 구조 불일치 수정 — manifest JSON 기반으로 통일\n- 모듈/플러그인/템플릿 install/activate/deactivate/uninstall 직후 라우트 미반영 수정\n- 다국어 번역 캐시 경합으로 raw key 노출되던 문제 수정 (engine-v1.38.1)\n- 조건부 `$t:` 표현식에서 번역 실패하던 문제 수정 (engine-v1.38.2)\n- 테스트 환경과 개발 환경이 동일한 Redis 캐시를 공유하여 상호 오염되던 문제 수정\n- 대시보드 브로드캐스트 스케줄이 무한 락에 걸리던 문제 수정\n- 환경설정 저장 후 `config:cache` 상태에서 변경값 미반영 수정\n- Windows 환경에서 파일 잠금 감지 시 30초+ hang 수정 — 0.5초로 단축\n- 개발 의존성 없이 설치된 프로덕션 환경에서 샘플 시더와 팩토리가 실행되지 않던 문제 수정 — 한국어 샘플 데이터 생성기가 자동으로 대체 동작하도록 개선\n- `sudo php artisan core:update` 실행 시 vendor 디렉토리 전체가 root 소유로 오염되던 문제 수정\n- 코어 업데이트 후 신규 권한·메뉴가 DB에 반영되지 않아 관리자 환경설정 페이지 접근이 거부되던 문제 수정\n- beta.1에서 업그레이드 시 제거된 메일 템플릿 모델 참조로 Fatal 오류가 발생하던 문제 수정\n- 업그레이드 후 `bootstrap/cache` 등 일부 디렉토리가 root 소유로 남아 확장 설치·업데이트가 거부되던 문제 수정 — 소유권 복원 범위를 설치 안내 경로 전체로 확장\n- 코어 업데이트 진행률 표시와 upgrade step 실행 로그가 같은 줄에 뒤섞이던 문제 수정 — 각 step 안내가 별도 줄로 깔끔하게 출력됨\n- 템플릿/레이아웃 공개 API가 확장 활성화 전·설치 중에 받은 \"찾을 수 없음\" 에러 응답을 영구 캐시하여 복구 후에도 같은 오류를 반환하던 문제 수정 — 에러 응답은 캐시에서 제외\n- `_pending`/`_bundled` 에 업데이트 중 남은 임시 디렉토리(`{id}_YYYYMMDD_HHMMSS`, `{id}_updating_*` 등) 의 원본 manifest 때문에 `install` 이 존재하지 않는 표준 경로로 접근해 실패하던 문제 수정 — 디렉토리명과 identifier 불일치 시 스캔에서 제외\n- 업그레이드 후 일부 코어 권한이 DB에 반영되지 않아 사용자 역할의 알림 기능 권한이 비어있던 문제 수정 — 별도 프로세스 실행으로 최신 로직 기반 재동기화 보장\n- 코어·확장 설정에서 제거된 메뉴·권한·역할·알림 정의·알림 템플릿·게시판 유형·클레임 사유가 업데이트 후에도 DB에 잔존하던 문제 수정 — 고아 레코드 자동 정리\n- 관리자 UI에서 메뉴·역할·알림·게시판 유형·클레임 사유·배송 유형 등을 수정해도 다음 업데이트 시 기본값으로 덮어써지던 문제 수정 — 사용자가 수정한 필드는 모든 저장 경로에서 자동 추적되어 업데이트 후에도 보존\n- 알림 정의·템플릿도 업데이트 기준으로 동기화 — 새 버전에서 제거된 알림은 DB에서도 삭제\n- 관리자 모듈/플러그인 목록의 \"작성자\" 표기가 manifest 의 `vendor` 필드 대신 식별자 prefix 를 사용하던 문제 수정 — `vendor` 가 정의된 확장은 정확한 작성자명으로 표시 (#9 @glitter-gim 님께서 제보해주셨습니다.)\n\n### Removed\n\n- MailTemplate 시스템 일괄 제거 — 모델, 컨트롤러, 서비스, 리스너, 시더, 팩토리, 다국어 파일 등. NotificationDefinition + NotificationTemplate 체계로 완전 대체\n\n> 참고: beta.1에서 업그레이드하는 운영 환경의 데이터 이관은 `Upgrade_7_0_0_beta_2`가 자동 처리합니다.\n\n## [7.0.0-beta.1] - 2026-04-01\n\n### Changed\n\n- 오픈 베타 릴리즈\n\n## [7.0.0-alpha.21] - 2026-03-30\n\n### Added\n\n- 인스톨러 PHP CLI/Composer 필수 검증 기능 — 기본 `php` 미감지 시 CLI 설정 필수 전환, Composer 실행 확인 및 미설치 시 설치 안내, 둘 다 검증 완료 전 다음 단계 진행 차단\n- 템플릿 레이아웃 수정 감지 시스템 — `original_content_hash`/`original_content_size` 컬럼 추가, SHA-256 해시 기반 수정 감지로 `updated_by` 방식 대체 (TemplateManager, LayoutRepository, 마이그레이션)\n- 관리자 로그인 화면 개선 — 비밀번호 찾기/재설정 플로우 추가, 테마 Segmented Control, 언어 셀렉트 확대 (PasswordResetController, PasswordResetNotification, ForgotPasswordRequest, ResetPasswordRequest)\n- `{{raw:expression}}` 바인딩 번역 면제 마커 시스템 — `$t:` 자동 번역 대상에서 특정 바인딩을 제외하는 마커 도입 (rawMarkers.ts)\n- 레이아웃 프리뷰 모드 — 관리자 레이아웃 편집기에서 실시간 미리보기 지원 (LayoutPreviewController, LayoutPreviewService, 마이그레이션)\n- 활동 로그 이력 조회 메뉴 신설 — ActivityLogController, ActivityLogResource, ActivityLogService, ActivityLogRepository, config/core.php 메뉴/권한 등록, 관리자 레이아웃 추가\n\n### Changed\n\n- `window.G7Config` 설정 노출 최소화 Phase 1 — 프론트엔드 미참조 설정의 브라우저 소스 노출 차단\n - SettingsService: 필드 레벨 `expose: false` 지원 추가 (formatCategorySettings)\n - FiltersFrontendSchema: `fields: {}` (빈 객체) 시 전체 차단 안전 기본값 적용\n - View Composers: TemplateComposer/UserTemplateComposer에 ModuleSettingsService 주입 — frontend_schema 기반 필터링 적용 (기존 config() 직접 참조 우회 수정)\n - 코어 defaults.json: general 4개 필드, security 전체, seo 전체, advanced 9개 필드(debug_mode 제외), upload 2개 필드, drivers 전체를 `expose: false` 처리\n- 캐시 무효화 로직 정비 — 016862cb 이전의 단순한 방식으로 복귀 (버전 증가 + TTL 자연 만료), `extension_cache_previous_versions` 추적 및 `getCacheVersionsToInvalidate()`/`getCacheVersionsForLayoutInvalidation()` 제거, Cache Tags 드라이버 분기 제거로 캐시 드라이버 비의존성 확보\n\n### Fixed\n\n- MariaDB를 `DB_CONNECTION=mysql` 드라이버로 연결 시 `WITH PARSER ngram` 에러로 인스톨러 설치 실패 수정 — `isMariaDb()` 메서드 추가하여 서버 버전 문자열 기반 실제 DBMS 감지 (DatabaseFulltextEngine)\n- 캐시 무효화 버전 키 누락 수정 — `warmTemplateCache()`가 버전 포함 키(`.v{version}`)로 캐시를 생성하나 `clearTemplateCache()`가 레거시 키만 삭제하던 문제, routes/language 캐시에 버전 포함 키 삭제 추가\n- 캐시 버전 0 무효화 누락 수정 — `array_filter()`가 버전 `0`을 falsy로 제거하여 `.v0` 캐시 키가 삭제 대상에서 누락되던 버그 (ClearsTemplateCaches, InvalidatesLayoutCache)\n- 테스트 활성 디렉토리 보호 강화 — `ProtectsExtensionDirectories`에 `moveDirectory` spy 추가, `copyToActive()` 원자적 교체가 활성 디렉토리를 rename으로 파괴하는 것 방지\n- 활동 로그 하위 호환 — `ActivityLogResource`에서 기존 DB 레코드(`type: 'text'`)의 enum 필드를 모델의 현재 `$activityLogFields` 기준으로 동적 번역 + 일괄 변경 `:count` 미치환 보정\n- 활동 로그 enum 미번역 전수 수정 — 14개 모델 필드의 `$activityLogFields` `type: 'enum'` 전환 + 14개 Enum 클래스에 `labelKey()` 메서드 추가 (User, Order, OrderOption, Product, Coupon, Post, Comment, Board)\n- 활동 로그 삭제/일괄삭제 `description_key` 및 샘플 시더 누락 추가 — 코어 시더 `activity_log_descriptions` 테이블에 delete/bulk_delete 키 보충 + 다국어 파일 동기화\n- 관리자 다크모드 품질 전수 조사 및 수정 — 레이아웃 JSON ~546건, TSX ~37건, CSS 5건의 누락된 `dark:` variant 추가 (이커머스 101파일, 게시판 19파일, 페이지 2파일, 마케팅 1파일, 템플릿 컴포넌트 ~20파일)\n- 레이아웃 편집 캐시 무효화 및 버전 히스토리 수정 — LayoutService의 저장/복원 시 PublicLayoutController 서빙 캐시 무효화 + `extension_cache_version` 증가 누락 수정\n- 사용자 관리에서 관리자/슈퍼관리자 삭제 시 토스트 메시지 및 어빌리티 처리 — CannotDeleteAdminException 추가, UserResource에 `can_delete` 어빌리티 반영\n- ActivityLog 핸들러 `$result` 타입 불일치 수정 — CoreActivityLogListener에서 `bool` → `array` 반환 타입 정합성 보정\n- 환경설정 사이트 로고 저장 시 Attachment 객체 정수 검증 오류 수정 — SaveSettingsRequest에서 Attachment JSON 객체를 정수로 검증하던 문제 수정\n- 관리자 SPA 네비게이션 로고 깜빡임 수정 — extends base 컴포넌트 불필요 remount 방지 (`_fromBase` 마킹 기반 stable key 패턴)\n- 인스톨러 다크모드 적색 배경 가독성 개선 — `alert-title`, `alert-message`, `permission-badge`, `test-result` 다크모드 색상 override 추가\n- 인스톨러 `BASE_PATH` 심볼릭 링크 미해석 — `realpath()` 적용 + 절대경로/상대경로 병기로 호스팅 환경 대응\n- 인스톨러 403 에러 페이지 다크모드 미지원 — `prefers-color-scheme: dark` 미디어쿼리 추가\n- 템플릿 업데이트 수정 감지 실패 — `LayoutRepository::update()`가 `updated_by`를 설정하지 않아 `hasModifiedLayouts()`가 항상 \"수정 없음\" 반환하던 문제 수정 (hash 비교 방식으로 전환)\n- 템플릿 업데이트 \"수정 유지\" 전략 미작동 — `layoutStrategy === 'keep'` 시 `refreshTemplateLayouts()` 미호출 → 양쪽 전략 모두 호출하되 `preserveModified` 플래그로 분기\n- 템플릿 업데이트 API 응답 키 불일치 — 백엔드 `has_modified` → 프론트엔드 기대 `has_modified_layouts`/`modified_count` 키 정렬\n- 템플릿/모듈/플러그인 설치 시 `incrementExtensionCacheVersion()` 누락 수정 — TemplateManager(install/activate/deactivate/uninstall) + ModuleManager(install) + PluginManager(install) 총 7곳 추가\n- 캐시 버전 변경 시 프론트엔드 다국어 미갱신 수정 — TemplateApp.ts에서 캐시 버전 변경 감지 시 TranslationEngine 재로드 추가\n- `warmTemplateCache()` 다국어 캐시 워밍 시 `$partial` 디렉티브 미해석 수정 — `json_decode`만 수행하던 코드를 `TemplateService::getLanguageDataWithModules()` 호출로 교체하여 fragment 해석 및 모듈/플러그인 다국어 병합 정상화\n- `ActivateTemplateCommand` 의존성 미충족 시 성공으로 보고하는 버그 수정 — `if ($result)` → `if ($result['success'])` 변경 및 의존성 경고 메시지 출력, `--force` 옵션 추가\n- `ActivateModuleCommand`/`ActivatePluginCommand` 의존성 미충족 시 경고 메시지 미출력 수정 — 의존성 경고 표시 및 `--force` 옵션 전달 추가\n\n## [7.0.0-alpha.20] - 2026-03-30\n\n### Added\n\n- Laravel Scout 통합 및 MySQL FULLTEXT(ngram) 검색엔진 드라이버 확장 시스템 도입 — 커스텀 `DatabaseFulltextEngine`으로 `MATCH...AGAINST IN BOOLEAN MODE` 지원, `core.search.engine_drivers` 필터 훅으로 Meilisearch/Elasticsearch 등 외부 엔진 플러그인 등록 가능\n- `DatabaseFulltextEngine` 다중 DBMS 호환 — FULLTEXT 미지원 DBMS(PostgreSQL, SQLite)에서 LIKE fallback 자동 전환, `whereFulltext()` 정적 헬퍼(관계 검색용), `addFulltextIndex()` 마이그레이션 헬퍼(DBMS별 조건부 DDL)\n- `config/scout.php` 설정 파일 추가 — 기본 드라이버 `mysql-fulltext`, `SCOUT_DRIVER` 환경변수로 전환 가능\n- `FulltextSearchable` 인터페이스 — FULLTEXT 검색 대상 컬럼 및 가중치 정의 계약\n- `AsUnicodeJson` 커스텀 캐스트 — JSON 컬럼에 한글을 `\\uXXXX` 이스케이프 없이 실제 UTF-8로 저장하여 FULLTEXT ngram 토크나이저 정상 동작 보장\n- 환경설정 > 드라이버 탭에 검색엔진 설정 카드 추가 — 기본 MySQL FULLTEXT(ngram) 드라이버 표시, 플러그인 설치 시 추가 드라이버 자동 표시\n- `SaveSettingsRequest`에 검색엔진 드라이버 동적 validation 추가 — `core.search.engine_drivers` 필터 훅 기반 허용 목록\n\n## [7.0.0-alpha.19] - 2026-03-29\n\n### Added\n\n- 검색/필터/정렬 성능 향상을 위한 누락 인덱스 일괄 추가 — activity_logs(description_key), users(created_at), mail_send_logs(status), template_layouts(template_id), schedules(created_at)\n\n## [7.0.0-alpha.18] - 2026-03-26\n\n### Added\n\n- SEO ExpressionEvaluator 산술 연산자 확장 — `*`, `/`, `%` 추가 (기존 `+`, `-`만 지원)\n- SEO PipeRegistry 파이프 함수 엔진 구현 — 프론트엔드 PipeRegistry.ts 빌트인 파이프 15종 PHP 미러링 (date, datetime, relativeTime, number, truncate, uppercase, lowercase, stripHtml, default, fallback, first, last, join, length, filterBy, keys, values, json, localized)\n- ActivityLog `description_params` ID→이름 변환 필터 훅 (`core.activity_log.filter_description_params`) — `ActivityLog::getLocalizedDescriptionAttribute()`에서 실행\n- 코어 모델 `$activityLogFields` 정의 — `User`, `Role`, `Menu`, `Schedule`, `MailTemplate` (5개 모델)\n- ActivityLog ChangeDetector 필드 라벨 다국어 키 추가 (`lang/ko/activity_log.php`, `lang/en/activity_log.php` — `fields` 섹션)\n- `module_helpers.php` — `getModuleSetting()` 헬퍼 함수 추가\n- CoreActivityLogListener: bulk_update per-User/per-Schedule 전환 + bulk_delete per-Schedule 전환 (건별 loggable_id 기록)\n- ActivityLogHandler: 삭제된 엔티티용 loggable_type/loggable_id 직접 지정 fallback 지원\n- 메뉴 관리 크로스 depth 이동 지원 — `UpdateMenuOrderRequest`에 `moved_items` 검증 추가, `MenuRepository`에 크로스 depth reorder 로직 구현\n- `NotCircularParent` 검증 규칙 추가 — 메뉴 순환 참조 방지\n\n### Changed\n\n- ActivityLog 규정 문서 대폭 보강 (`docs/backend/activity-log.md`) — `description_params` 저장 정책, `ActivityLogDescriptionResolver` 패턴, Bulk Update ChangeDetector 패턴, 개발자 체크리스트 추가\n\n## [7.0.0-alpha.17] - 2026-03-26\n\n### Added\n\n- ActivityLog `description_params` ID→이름 변환 필터 훅 (`core.activity_log.filter_description_params`) — `ActivityLog::getLocalizedDescriptionAttribute()`에서 실행\n- 코어 모델 `$activityLogFields` 정의 — `User`, `Role`, `Menu`, `Schedule`, `MailTemplate` (5개 모델)\n- ActivityLog ChangeDetector 필드 라벨 다국어 키 추가 (`lang/ko/activity_log.php`, `lang/en/activity_log.php` — `fields` 섹션)\n- `module_helpers.php` — `getModuleSetting()` 헬퍼 함수 추가\n- CoreActivityLogListener: bulk_update per-User/per-Schedule 전환 + bulk_delete per-Schedule 전환 (건별 loggable_id 기록)\n- ActivityLogHandler: 삭제된 엔티티용 loggable_type/loggable_id 직접 지정 fallback 지원\n\n### Changed\n\n- ActivityLog 규정 문서 대폭 보강 (`docs/backend/activity-log.md`) — `description_params` 저장 정책, `ActivityLogDescriptionResolver` 패턴, Bulk Update ChangeDetector 패턴, 개발자 체크리스트 추가\n\n## [7.0.0-alpha.17] - 2026-03-26\n\n### Added\n\n- Monolog 기반 ActivityLog 아키텍처: `Log::channel('activity')` → `ActivityLogHandler` → DB (3단계)\n- `ActivityLogChannel` (커스텀 Monolog 채널), `ActivityLogHandler`, `ActivityLogProcessor` 신규\n- ActivityLog i18n 지원: `description_key` + `description_params` 기반 실시간 다국어 번역\n- ActivityLog 구조화된 변경 이력: `changes` JSON 컬럼 (필드별 `label_key`, `old`/`new`, `type` 포함)\n- `ChangeDetector` 유틸리티 (모델 스냅샷 비교 → 구조화된 변경 이력 생성)\n- `CoreActivityLogListener` 전면 확장: 모든 코어 Service 훅 구독 (User/Role/Menu/Settings/Schedule/Auth/Module/Plugin/Template/Layout/MailTemplate/Attachment — 66개 훅)\n- 활동 로그 다국어 키 105개 정의 (`lang/ko/activity_log.php`, `lang/en/activity_log.php`)\n- `config/logging.php`에 `activity` 채널 추가\n- `config/activity_log.php` 전용 설정 파일 신규\n- `activity_logs` 테이블 복합 인덱스 추가 (`loggable_type`+`loggable_id`+`created_at`, `log_type`+`action`+`created_at`)\n\n### Fixed\n\n- `resolveLogType()` 사용자 역할 기반 → 요청 경로 기반으로 변경: 관리자가 사용자 화면에서 수행한 액션이 `admin`으로 기록되던 문제 수정 (ResolvesActivityLogType)\n\n### Changed\n\n- `ActivityLog` 모델: `description` 컬럼 삭제 → `description_key`/`description_params` 기반 다국어 전환\n- `ActivityLogService`: 기록 메서드 전면 제거 → 조회 전용으로 축소\n- `ActivityLogResource`: `description` → `localized_description` (실시간 번역)\n- 모든 Controller에서 `logAdminActivity()` 호출 전면 제거 → Listener 경로로 전환\n\n### Removed\n\n- `activity_logs.description` 컬럼 (DB 삭제)\n- `ActivityLogManager`, `ActivityLogDriverInterface`, `DatabaseActivityLogDriver`, `NullActivityLogDriver`\n- `ActivityLogListener` (이중 훅 계층 — Monolog Handler로 대체)\n- `ActivityLogService.log`/`logAdmin`/`logUser`/`logSystem` (Monolog 채널로 대체)\n- `AdminBaseController.logAdminActivity()`, `generateActivityDescription()`, `flattenDataForTranslation()`\n\n### Fixed\n\n- 마이페이지 프로필 저장 시 국가·언어 미선택 상태에서 오류가 발생하던 문제 수정 — 해당 필드를 선택 사항으로 변경\n- 인스톨러 Step 2 .env 복사 명령어 안내 수정 — `.env.example.production` → `.env.example` (functions.php 2곳, installer.js 4곳)\n\n## [7.0.0-alpha.16] - 2026-03-23\n\n### Fixed\n\n- 코어 업데이트 롤백 시 vendor 디렉토리 복원 불가 수정 — 백업 targets에 vendor 포함, excludes에서 vendor 제거\n- 코어 백업 복원 시 개별 target 실패가 전체 복원을 중단하는 문제 수정 — 개별 try-catch로 나머지 target 복원 계속 진행\n- 코어 업데이트 롤백 실패 시 수동 복구 안내 미출력 수정 — composer install 등 복구 단계 안내 추가\n- 코어 업데이트 완전 복원 성공 시 유지보수 모드 자동 해제 추가\n- 코어 업데이트 시 vendor 디렉토리 이중 처리로 인한 마이그레이션 실패 수정 — `backup_only` 설정 분리 (applyUpdate 제외, 백업/복원 전용)\n\n## [7.0.0-alpha.15] - 2026-03-23\n\n### Added\n\n- Users UUID 전환 — 외부 노출 ID를 UUID v7으로 전환, 정수 `id`는 API 응답에서 숨김\n- UniqueIdService 코어 서비스 추가 (UUID v7 + NanoID 생성)\n- 코어 업그레이드 스텝 추가 (Upgrade_7_0_0_beta_15)\n\n### Changed\n\n- User 모델: `getRouteKeyName()` → 'uuid', `$hidden`에 'id' 추가\n- 공개 프로필 API: Route Model Binding 전환 (`{userId}` → `{user}`)\n- 사용자 벌크 상태변경: 정수 ID → UUID 기반\n- API Resource: user.id → user.uuid 전환 (UserResource, UserCollection 외 7개)\n- Activity Log 메타데이터: user_id → uuid 전환\n- FormRequest: 정수 검증 → UUID 검증 전환\n\n### Fixed\n\n- `_global.currentUser?.id` → `?.uuid` 전환 (sirsoft-basic 템플릿 12개 파일)\n- 글쓰기 버튼 abilities 비활성화 누락 수정\n- 코어 업그레이드 스텝 raw SQL 테이블 프리픽스 미적용 수정 — `UpgradeContext::table()` 헬퍼 추가 (Upgrade_7_0_0_beta_15)\n\n## [7.0.0-alpha.14] - 2026-03-20\n\n### Fixed\n\n- 확장 업데이트 시 임시 디렉토리(`_updating_*`, `_old_*`) 오토로드 오염 방지 — 임시 디렉토리를 `_pending/` 하위에 생성하여 IDE 잠금 등으로 잔존 시에도 Fatal Error 방지 (ExtensionPendingHelper)\n\n### Added\n\n- Windows 파일잠금 감지/해제 기능 — 확장 업데이트 시 IDE 등이 파일 핸들을 보유한 경우 자동 감지 및 해제 시도 (FileHandleHelper, ExtensionPendingHelper)\n\n- SEO 렌더러 훅 시스템: `core.seo.filter_context`, `core.seo.filter_meta`, `core.seo.filter_view_data` — 확장이 SEO 렌더링 파이프라인에 런타임 데이터 변환으로 개입 가능\n- `seo.blade.php` 확장 슬롯: `extraHeadTags` (`` 직전), `extraBodyEnd` (`` 직전) — `filter_view_data` 훅을 통해 커스텀 스크립트/스타일 주입\n- `ComponentHtmlMapper` pagination 렌더 모드 — Pagination 컴포넌트에서 SEO용 페이지 링크 자동 생성 (currentPage/totalPages props 기반)\n- `ComponentHtmlMapper` text_format dot notation 지원 — `{author.nickname}` 형태로 객체 prop의 중첩 필드 접근\n- `ExpressionEvaluator::evaluateRaw()` — 표현식 결과를 원본 타입(배열 등)으로 반환하는 메서드\n- `SeoRenderer` SEO 컨텍스트에 `_global`/`_local` 빈 객체 추가 — 프론트엔드 전용 상태 참조 시 null 대신 빈 객체 제공\n- `ComponentHtmlMapper` fields 렌더 모드 — 컴포지트 컴포넌트(ProductCard 등)의 객체 prop에서 SEO용 HTML 필드 자동 생성 (조건부/반복/속성 기반)\n- `SeoRenderer` seoVars 주입 — `meta.seo.vars` 선언을 해석하여 ComponentHtmlMapper format 모드에서 `{key}` 플레이스홀더 치환\n- `ExpressionEvaluator` 리터럴 값 감지 — 숫자, 문자열, boolean, null/undefined 리터럴을 경로 해석 없이 직접 반환\n- `ExpressionEvaluator` $t: 파라미터 `{{}}` 표현식 해석 — 번역 키 파라미터 값에 포함된 바인딩 표현식을 컨텍스트에서 평가\n- `TemplateManager` seo-config 검증에 `fields` 타입 추가\n- `ExpressionEvaluator` seo_overrides — seo-config.json에서 `_local`/`_global` 상태 오버라이드 선언 (와일드카드 매칭으로 접혀있는 콘텐츠 SEO 강제 펼침)\n- `TemplateManager` seo-config 검증에 `pagination` 타입 및 `seo_overrides` 검증 추가\n- `SeoConfigMerger` — 모듈/플러그인/템플릿의 seo-config.json을 수집·병합하는 동적 확장 시스템 (우선순위: 모듈 → 플러그인 → 템플릿, 24시간 TTL 캐싱)\n- `SeoRenderer` `_global` 컨텍스트 주입 — SettingsService/PluginSettingsService 프론트엔드 설정을 `_global`에 주입 + `initGlobal` 매핑으로 데이터소스 응답을 `_global` 경로에 바인딩\n- `AbstractModule`/`AbstractPlugin` SEO 기여 메서드 — `getSeoConfig()`, `getSeoDataSources()` 인터페이스 추가\n- `ModuleManager`/`PluginManager`/`TemplateManager` install/activate/update 시 훅 발행 추가 — Artisan 커맨드에서도 SEO 캐시 자동 무효화 보장\n- SEO Artisan 커맨드 다국어 파일 추가 (`lang/ko/seo.php`, `lang/en/seo.php`)\n- `ComponentHtmlMapper` fields 모드 `$all_props` source — 모든 props 표현식을 해석하여 데이터 객체로 사용 (Header/Footer 등 다수 props 컴포넌트용)\n- `ComponentHtmlMapper` fields 모드 `$t:` 번역 키 지원 — content 패턴에서 `$t:key` → 다국어 텍스트 렌더링\n- `ComponentHtmlMapper` fields iterate `item_attrs` — 아이템별 동적 HTML 속성 (예: `{ \"href\": \"/board/{slug}\" }`)\n- `ExpressionEvaluator` `evaluateRaw()` `??` null coalescing 지원 — 원본 타입(배열/객체) 유지하면서 null coalescing 수행\n- 인스톨러 Windows 환경 명령어 대응 — `chmod`/`chown` 스킵, 미존재 디렉토리 안내 메시지 추가\n- `ExpressionEvaluator` 삼항 연산자 (`a ? b : c`) — JS 우선순위 준수, `?.`/`??` 자동 구분, 중첩 우측 결합\n- `ExpressionEvaluator` `$t()` 함수 호출 구문 — 삼항 내부에서 `$t('key')` 형태로 번역 키 사용 가능 (기존 `$t:key` 방식 확장)\n- `ExpressionEvaluator` `$localized()` 전역 함수 — 다국어 객체에서 현재 로케일 값 추출 (`{ko: \"상품\", en: \"Product\"}` → `\"상품\"`)\n- `ExpressionEvaluator` 객체 리터럴 파서 — `{key: value, ...obj, [dynamicKey]: value}` 구문 지원\n- `ExpressionEvaluator` 스프레드 연산자 — 배열 `[...arr, item]` 및 객체 `{...obj, key: value}` 스프레드 지원\n- `SeoRenderer` computed 속성 해석 — 레이아웃 `computed` 섹션을 `_computed`/`$computed`에 저장 (문자열 표현식 + `$switch` 형식)\n- `ComponentHtmlMapper` classMap 지원 — `base`/`variants`/`key`/`default`로 조건부 CSS 클래스 선언적 적용\n\n### Fixed\n\n- Redis 캐시 DB가 환경설정 값(`REDIS_CACHE_DB`)을 따르도록 수정 — `config/database.php` cache 연결 DB 반영\n\n- `ExpressionEvaluator` 배열 리터럴 파싱 지원 — `['gallery','card'].includes(...)` 표현식에서 배열 리터럴을 PHP 배열로 변환하여 `includes` 등 배열 메서드 정상 동작 (SEO 게시판 타입 분기 조건 중복 렌더링 수정)\n- `ExpressionEvaluator` null 비교 JavaScript 시맨틱 적용 — `null !== value` → `true`, `null === null` → `true` (SEO 컨텍스트에서 `_global` 미존재 경로 비교 시 빈 문자열 대신 올바른 boolean 반환)\n- `ExpressionEvaluator` $t: 번역에서 `{{param}}` 형식 파라미터 치환 지원 — 템플릿 번역 파일의 `{{param}}` + Laravel 표준 `:param` 형식 모두 처리\n- `ExpressionEvaluator` 비교 연산 좌측 optional chaining 경로 타입 보존 — `?.` 포함 경로가 `evaluateExpression`으로 불필요 라우팅되어 boolean 타입이 문자열로 변환되던 문제 수정\n\n## [7.0.0-alpha.13] - 2026-03-18\n\n### Added\n\n- `SeoCacheRegenerator` — 단건 URL 캐시 즉시 재생성 서비스 (다국어 로케일별 렌더링 + 캐시 저장)\n- `SeoSettingsCacheListener` — 코어 SEO 설정 변경 시 전체 SEO 캐시 + 사이트맵 삭제\n\n### Fixed\n\n- `SeoMiddleware`에서 `put()` 대신 `putWithLayout()` 사용 — `invalidateByLayout()`이 레이아웃명 미저장으로 항상 0건 매칭되던 근본 버그 수정\n- `SeoRenderer`에서 레이아웃명을 request attribute로 저장 — `putWithLayout()` 연동\n\n## [7.0.0-alpha.12] - 2026-03-17\n\n### Changed\n\n- API 인증을 토큰 전용으로 전환 — 세션 기반 인증 의존 제거, Bearer 토큰 단일 방식으로 통일\n- README 업데이트 — 표기 통일, 섹션 정비, 문서 링크 연결\n\n## [7.0.0-alpha.11] - 2026-03-16\n\n### Added\n\n- 역할(Role) 상태 토글 API 추가 — `PATCH /api/admin/roles/{role}/toggle-status` 엔드포인트, 훅 지원 (`core.role.before_toggle_status`, `core.role.after_toggle_status`)\n- RoleResource에 `can_toggle_status` ability 추가 — 역할별 토글 권한 제어\n- RoleResource에 `extension_name` 필드 추가 — 확장 출처별 로케일 이름 표시\n- 역할 생성 시 `identifier` 직접 입력 기능 추가 — 미입력 시 name 기반 자동 생성 유지\n- 역할 관련 다국어 키 추가 (ko/en) — identifier 검증 메시지\n- `replaceUrl` 핸들러 추가 — refetch 없이 URL만 변경 (페이지네이션 등 브라우저 히스토리 관리용)\n- 사용자 검색 API에 `id` 파라미터 지원 추가 — 특정 사용자 ID로 직접 조회 가능\n- `TimezoneHelper` 유틸리티 클래스 추가 — 사용자/서버 타임존 간 변환 헬퍼\n- `HasSampleSeeders` 트레이트 추가 — 모듈/플러그인 시더에서 `--sample` 옵션으로 샘플 데이터만 분리 실행\n- `ComponentRegistry.getComponentNames()` 메서드 추가 — 등록된 컴포넌트 이름 목록 조회\n\n### Fixed\n\n- 템플릿 에러 페이지에서 `:identifier`가 리터럴로 표시되는 버그 수정 — Blade `@` 이스케이프 처리\n- ActionDispatcher onChange raw value fallback 제거 — 마운트/리렌더 시 의도치 않은 setState 실행으로 상품 폼 회귀 유발\n- Form 자동 바인딩 setState 경합 수정 — 자동 바인딩과 수동 setState가 동시 실행 시 stale 값으로 덮어쓰이는 문제 해결\n- Form 자동 바인딩 bindingType 메타데이터 기반 boolean 바인딩 수정 — Toggle/Checkbox 등 boolean 컴포넌트에서 문자열 변환 대신 boolean 값 유지\n- SPA 네비게이션 시 `_global._local`에 이전 페이지 상태가 잔존하는 버그 수정 — 페이지 전환 시 `_local` 초기화 처리\n- DynamicRenderer `_computed` 참조 prop에서 캐시된 stale 값이 사용되는 버그 수정 — `_computed`/`$computed` 참조 시 `skipCache: true` 적용\n\n### Changed\n\n- 마이그레이션 통합 — 증분 마이그레이션을 테이블당 1개 create 마이그레이션으로 정리\n- 시더 디렉토리 분리 — 설치 시더와 샘플 시더를 `Sample/` 하위로 분리, `--sample` 옵션 추가\n- 라이선스 프로그램 명칭 정비\n- 일정 관리 메뉴 기본 비활성화 (`config/core.php`)\n- Composer 의존성 업데이트 — Laravel Framework v12.54.1, Reverb v1.8.0, Symfony v7.4.6~7 등\n\n## [7.0.0-alpha.9] - 2026-03-13\n\n### Added\n\n- 그누보드7 커스텀 에러 페이지 도입 — Laravel 기본 에러 페이지(401, 403, 404, 500, 503)를 그누보드7 스타일로 교체, 다크 모드 지원, 접근 경로 기반 홈 링크 분기 (admin → `/admin`, 기타 → `/`)\n- 환경설정 > 고급 디버그 모드 활성화 시 개발 대시보드(`/dev`) 바로가기 버튼 추가\n- 루트 LICENSE 파일 생성 (MIT 라이선스, 한국어 번역 + 영문 원문)\n- 코어 라이선스/Changelog API 엔드포인트 추가 (`GET /api/admin/license`, `GET /api/admin/changelog`)\n- 확장 라이선스 API 엔드포인트 추가 (`GET /api/admin/modules/{id}/license`, `GET /api/admin/plugins/{id}/license`, `GET /api/admin/templates/{id}/license`)\n- Admin 푸터 copyright 클릭 → 코어 라이선스 모달, 버전 클릭 → Changelog 모달 표시 기능\n- 확장 상세 모달에서 라이선스 클릭 시 전문 모달 표시 기능 (모듈/플러그인/템플릿)\n- 각 번들 확장에 LICENSE 파일 및 manifest `license` 필드 추가\n\n### Changed\n\n- 설치 화면 라이선스를 루트 LICENSE 파일로 통합 (`public/install/lang/license-ko.txt`, `license-en.txt` 삭제)\n- `/dev` 라우트 뷰 이름을 `dev-dashboard`로 변경\n\n## [7.0.0-alpha.8] - 2026-03-13\n\n### Changed\n\n- `.env.example.develop`과 `.env.example.production`을 `.env.example`로 통합 — 설치형 솔루션에 환경별 분리 불필요, Laravel/Vite 표준 준수\n- 인스톨러에서 `.env.production` 백업 파일 생성 로직 제거 — Vite mode 기반 로딩 충돌 방지\n\n### Improved\n\n- 코어/확장 업데이트 시 composer install 스킵 최적화 — composer.json/composer.lock 미변경 시 composer install 및 vendor 디렉토리 교체를 건너뛰어 업데이트 시간 단축\n\n### Fixed\n\n- 확장 수동 설치 모달에서 설치 실패 시 상세 에러 사유(`errors.error`) 미표시 문제 수정 — 3개 모달에 상세 에러 P 요소 추가\n- `checkDependencies()` 복수 의존성 에러 수집 — 첫 번째 미충족 의존성에서 즉시 throw 대신 전체 수집 후 줄바꿈 연결 (ModuleManager, PluginManager, TemplateManager)\n- ModuleController/PluginController 에러 반환 형식을 TemplateController와 통일 — `['error' => $e->getMessage()]` 형태\n- 확장 설치 실패 시 `_pending/{identifier}` 디렉토리 자동 정리 — ModuleService, PluginService, TemplateService에 try-catch 추가\n\n## [7.0.0-alpha.7] - 2026-03-13\n\n### Added\n\n- 템플릿 엔진 `multipart/form-data` 지원 — apiCall 핸들러 및 DataSourceManager에서 `contentType: \"multipart/form-data\"` 설정 시 params를 FormData로 자동 변환\n - ActionDispatcher: `fetchWithOptions()`에 FormData 변환 로직 추가, Content-Type 헤더 자동 생략 (브라우저 boundary 설정)\n - DataSourceManager: `toFormData()` 메서드 추가, 인증/비인증 경로 모두 multipart 지원\n - File/Blob 원본 유지, null/undefined 제외, 객체/배열 JSON.stringify 변환\n- `deepMergeWithState()` non-plain 객체(File/Blob/Date) 보호 — spread 복사로 인한 내부 데이터 소실 방지\n- `resolveParams()` non-plain 객체 재귀 해석 스킵 — File 객체가 빈 객체로 변환되는 문제 방지\n- 컴포넌트 onChange raw value fallback — FileInput, Toggle 등 Event가 아닌 값을 전달하는 컴포넌트 지원\n\n### Fixed\n\n- DataSourceManager `isMultipart` 변수 TDZ(Temporal Dead Zone) 버그 수정 — 선언 전 참조로 인한 ReferenceError 해결\n\n## [7.0.0-alpha.6] - 2026-03-13\n\n### Fixed\n\n- 플러그인 환경설정 페이지 진입 시 404 오류 수정 — `registerPluginLayouts()` admin/user 분기 도입 후 루트 `settings.json`이 스킵되던 문제\n - 플러그인 설정 레이아웃을 `resources/layouts/settings.json` → `resources/layouts/admin/plugin_settings.json`으로 이동하여 모듈과 동일한 구조로 통일\n - `AbstractPlugin::getSettingsLayout()` 경로 변경\n - `PluginSettingsService` 오버라이드 경로 및 주석 수정\n - 영향받는 플러그인: sirsoft-daum_postcode, sirsoft-marketing, sirsoft-tosspayments\n\n### Changed\n\n- 플러그인 설정 레이아웃 규정 문서(`plugin-development.md`) 경로/설명 업데이트\n\n## [7.0.0-alpha.5] - 2026-03-12\n\n### Added\n\n- 확장(모듈/플러그인/템플릿) changelog GitHub 원격 소스 지원 — `source=github` 시 GitHub에서 CHANGELOG.md 조회, 실패 시 bundled 폴백\n- `ChangelogParser::parseFromString()`, `getVersionRangeFromString()` 문자열 기반 파싱 메서드 추가\n- `ChangelogRequest` validation 에러 다국어 메시지 추가 (source, version format, required_with)\n- 플러그인 GitHub/ZIP 설치 기능 추가 — 모듈/템플릿에는 있지만 플러그인에 누락되어 있던 기능 신규 구현\n - `PluginService::installFromGithub()`, `installFromZipFile()`, `findPluginJson()` 메서드 추가\n - `PluginController::installFromFile()`, `installFromGithub()` 엔드포인트 추가\n - `InstallPluginFromGithubRequest`, `InstallPluginFromFileRequest` FormRequest 추가\n - `install-from-file`, `install-from-github` API 라우트 추가\n- `ExtensionManager::hasComposerDependenciesAt(string $path)` 메서드 추가 — 임의 경로의 Composer 의존성 확인\n- 모듈/플러그인 설치 시 `_pending` Composer 선행 설치 로직 추가 — 활성 디렉토리 이관 전 의존성 설치\n\n### Changed\n\n- 확장 수동 설치 모달 3개(모듈/플러그인/템플릿) UI 통일 — TabNavigation underline, 에러 배너, 필드별 적색 테두리\n- 확장 GitHub/ZIP 설치 공통 로직을 `GithubHelper`, `ZipInstallHelper`로 추출하여 3개 Service 중복 제거\n- `CoreUpdateService` GitHub 관련 protected 메서드를 `GithubHelper` 위임으로 리팩토링\n- 확장 목록 PageHeader에서 새로고침 버튼 제거, 업데이트 확인 버튼으로 통일\n- 코어 업데이트 `core_pending` 고정 경로 → `core_{Ymd_His}` 타임스탬프 기반 격리 디렉토리로 변경\n- 확장(모듈/플러그인/템플릿) 업데이트에 `_pending/{identifier}_{timestamp}/` 스테이징 패턴 도입\n- 확장 업데이트 시 스테이징 내에서 composer install 실행 (활성 디렉토리 무영향)\n- 확장 GitHub 다운로드 코드를 코어와 동일한 패턴으로 통합 (인증 헤더, 폴백 체인, 타임아웃)\n- GitHub 다운로드 공용 로직을 `ExtensionManager`로 추출하여 3개 Manager 중복 제거\n- `config/app.php`에서 `preserves` 설정 키 제거 (타임스탬프 격리로 불필요)\n- 모듈/플러그인 GitHub/ZIP 설치 흐름을 `temp → _pending → composer install → 활성 디렉토리` 패턴으로 통일\n\n### Fixed\n\n- 코어 업데이트 결과 모달에서 from/to 버전 파라미터가 전달되지 않는 버그 수정 — `params.params` → `params.query`\n- 플러그인에 GitHub/ZIP 설치 기능이 누락되어 있던 결함 수정\n- 파일 복사 시 퍼미션/소유자/소유그룹 미보존 문제 수정 — `File::copy()` → `FilePermissionHelper::copyFile()` 교체 (6개 위치)\n- Windows 환경에서 확장/코어 업데이트 후 `_pending` 하위에 빈 디렉토리가 잔존하는 문제 수정 — `cleanupStaging()`에 3단계 retry 로직 추가\n- 코어 업데이트 후 vendor 교체 시 stale `packages.php`/`services.php`로 인한 500 오류 수정 — `clearAllCaches()`에 컴파일 캐시 삭제 + `package:discover` + `extension:update-autoload` 추가\n- 코어 업데이트 시 `bootstrap/cache` 디렉토리가 소스에서 덮어씌워지는 문제 수정 — excludes에 `bootstrap/cache` 추가\n\n## [7.0.0-alpha.4] - 2026-03-12\n\n### Added\n\n- 코어 업그레이드 스텝 검증용 샘플 마이그레이션 및 업그레이드 스텝 추가\n- 코어 업그레이드 스텝 경로를 `database/upgrades/` → `upgrades/`로 변경\n- 업그레이드 스텝 실행을 프로그레스바 별도 단계로 분리 및 터미널 피드백 추가\n\n### Changed\n\n- `--source` 모드에서 원본 소스 디렉토리를 `_pending`으로 복제 후 작업 (원본 보호)\n- Step 8: 운영 디렉토리 `composer install` 재실행 → `_pending/vendor/` 복사로 변경 (효율화)\n\n### Fixed\n\n- `--source` 모드에서 소스 버전 감지 시 현재 `env()` 대신 `config/app.php` default 값 파싱\n- 코어 업데이트 targets에 `upgrades` 디렉토리 누락 수정\n\n## [7.0.0-alpha.3] - 2026-03-12\n\n### Fixed\n\n- `.gitattributes`의 `CHANGELOG.md export-ignore`가 모든 CHANGELOG 파일을 릴리스 아카이브에서 제외하던 문제 수정\n- PharData(tar ustar) 100바이트 경로 제한으로 324개 파일이 누락되어 orphan 삭제가 발생하던 버그 수정\n\n### Removed\n\n- PharData 아카이브 추출 전략 제거 — tar ustar 형식의 100바이트 경로 제한은 근본적 해결 불가\n\n### Added\n\n- `core:update --source=` 옵션 추가 — ZipArchive/unzip 불가 환경에서 수동 업데이트 지원\n - 상대경로, 절대경로, Windows 경로 모두 지원\n - 소스 디렉토리의 그누보드7 프로젝트 유효성 검증 (`config/app.php` + `version` 키)\n- 업데이트 안내 모달에 수동 업데이트 가이드 섹션 추가\n- 시스템 요구사항 미충족 시 `--source` 옵션 안내 메시지 추가\n\n## [7.0.0-alpha.2] - 2026-03-12\n\n### Fixed\n- 코어 업데이트 확인 시 업데이트할 버전이 없으면 피드백 없던 문제 수정\n - `openModal` 호출 형식 수정 (`params.id` → `target`)\n - 모달 데이터 바인딩 경로 수정 (`_local` → `$parent._local`)\n- 코어 업데이트 명령어 에러 미출력 수정\n- `.env` 예제 파일에서 `G7_UPDATE_TARGETS` 하드코딩 제거\n\n### Added\n- 코어 업데이트 targets 확장 및 orphan 삭제 로직 추가\n- 코어 업데이트 `--local` 옵션 및 설정 기반 제외/보존 추가\n- 환경설정 고급 탭 코어 업데이트 설정 섹션 추가\n\n## [7.0.0-alpha.1] - 2026-03-07\n\n### Added\n\n#### 코어 아키텍처\n\n- Laravel 12 기반 CMS 플랫폼 초기 구조 설계 및 구현\n- Service-Repository 패턴 기반 계층 분리 아키텍처 구축\n- CoreServiceProvider를 통한 인터페이스-구현체 바인딩 시스템\n- ResponseHelper를 통한 통일된 API 응답 형식 (success/error/paginated)\n- AdminBaseController / AuthBaseController / PublicBaseController 컨트롤러 계층 구조\n- FormRequest + Custom Rule 기반 검증 시스템\n- BaseApiResource 상속 기반 API 리소스 패턴\n- PHP 8.2+ Backed Enum 기반 상태/타입/분류 관리\n\n#### 확장 시스템 (Extension System)\n\n- 모듈(Module) 시스템: 디렉토리 스캔 기반 자동 발견, 설치/활성화/비활성화/삭제 관리\n- 플러그인(Plugin) 시스템: 모듈 의존 기반 기능 확장, 설정 UI(settings.json) 지원\n- 템플릿(Template) 시스템: Admin/User 타입 분리, JSON 기반 레이아웃, 컴포넌트 레지스트리\n- ExtensionManager: 모듈/플러그인/템플릿 통합 관리 (설치, 업데이트, 삭제)\n- HookManager: Action/Filter 훅 시스템 (doAction, applyFilters, HookListenerInterface)\n- 확장 업데이트 시스템: _bundled/_pending 디렉토리 구조, GitHub/로컬 업데이트 감지 및 적용\n- 확장 백업/복원 시스템 (ExtensionBackupHelper)\n- 확장 상태 가드 (ExtensionStatusGuard): Installing/Updating/Uninstalled 상태 관리\n- 확장 권한 동기화 (ExtensionRoleSyncHelper): 설치/삭제 시 역할-권한 자동 동기화\n- 확장 메뉴 동기화 (ExtensionMenuSyncHelper): 모듈 메뉴 자동 등록/해제\n- 확장 오토로드 시스템: 런타임 Composer 오토로드 (composer.json 수정 불필요)\n- 확장 Composer 의존성 관리 (extension:composer-install 커맨드)\n- 확장 업그레이드 스텝 시스템 (UpgradeStepInterface, UpgradeContext)\n- 확장 설정 시스템 (SettingsMigrator): 모듈/플러그인별 독립 설정 관리\n- 확장 소유권 시스템: extension_type/extension_identifier 기반 리소스 귀속\n- 확장 에셋 시스템: module.json 매니페스트 기반 JS/CSS 자동 로딩\n- 확장 Changelog 시스템: ChangelogParser 헬퍼 + API 엔드포인트 + 관리 화면 인라인 표시\n- 확장 빌드 시스템: Artisan 커맨드 기반 (module:build, template:build, plugin:build)\n- 확장 캐시 관리: 확장별 독립 캐시 + 일괄 클리어 커맨드\n\n#### 템플릿 엔진 (Template Engine)\n\n- DynamicRenderer: JSON 레이아웃 기반 React 컴포넌트 동적 렌더링\n- DataBindingEngine: `{{expression}}` 문법, Optional Chaining(`?.`), Nullish Coalescing(`??`) 지원\n- ActionDispatcher: 20+ 내장 핸들러 (navigate, apiCall, setState, openModal, closeModal, sequence, condition, replaceUrl, scrollTo, copyToClipboard, downloadFile, debounce, emit, showToast, confirm, validate, submit, reset, filter, sort 등)\n- ComponentRegistry: 컴포넌트 등록/검색/해석 시스템 (기본/집합/레이아웃 타입)\n- TranslationEngine: `$t:key` 즉시 평가 / `$t:defer:key` 지연 평가 다국어 바인딩\n- LayoutLoader: JSON 레이아웃 로딩, 캐싱, ETag 기반 조건부 요청\n- Router: SPA 라우팅, 동적 경로 파라미터(`{{route.id}}`), 쿼리스트링 관리\n- 레이아웃 상속 시스템: `extends` 기반 베이스 상속 + `type: \"slot\"` 위치에 컨텐츠 삽입\n- Partial 시스템: 레이아웃 모듈화, 컴포넌트 치환 (data_sources/computed/modals/state 미지원)\n- 조건부 렌더링: `if` 속성 기반 표현식 평가 (type: \"conditional\" 미지원)\n- 반복 렌더링: `iteration` 설정 (source, item_var, index_var, key)\n- 반응형 레이아웃: `responsive` 속성 기반 breakpoint 오버라이드 (portable/compact/wide)\n- 다크 모드: Tailwind `dark:` variant 기반 자동 전환\n- classMap: 조건부 CSS 클래스 매핑 (key → variants)\n- computed: 계산된 속성 시스템 (의존성 추적, 자동 재계산)\n- 모달 시스템: `modals` 섹션 + openModal/closeModal 핸들러 (`_global.modalStack` 기반)\n- init_actions: 레이아웃 초기화 시 자동 실행 액션 (루트 레이아웃 레벨)\n- Named Actions: 액션 재사용 시스템 (DRY 패턴)\n- errorHandling: 전역/데이터소스별 에러 핸들링 설정\n- scripts: 레이아웃 레벨 커스텀 스크립트 로딩\n- globalHeaders: API 호출 공통 헤더 설정 (pattern 기반 매칭)\n- blur_until_loaded: 데이터 로딩 전 블러 처리\n- lifecycle: 컴포넌트 생명주기 훅 (onMount, onUnmount)\n- slots: 컴포넌트 슬롯 시스템\n- layout_extensions: 모듈/플러그인의 동적 UI 주입 포인트\n- isolatedState: 컴포넌트 상태 격리\n- 데이터소스 조건부 로딩: `if` 표현식 기반 활성화/비활성화\n\n#### 컴포넌트 시스템\n\n- Basic 컴포넌트 (27+): Div, Button, Input, Select, Form, A, H1~H6, Span, P, Img, Label, Textarea, Table, Thead, Tbody, Tr, Th, Td, Ul, Ol, Li, Hr, Strong, Em, Small, Pre, Code, Blockquote, Nav, Header, Footer, Main, Section, Article, Aside, Figure, FigCaption\n- Composite 컴포넌트: DataGrid, CardGrid, Pagination, Modal, SearchBar, Tabs, TabPanel, Accordion, Badge, Breadcrumb, Card, Checkbox, CheckboxGroup, DatePicker, Dropdown, FileUpload, Icon, Notification, Radio, RadioGroup, RangeSlider, Rating, Select (enhanced), Sidebar, Stepper, Switch, Tag, Timeline, Toast, Tooltip, PasswordInput, DynamicFieldList, SortableList, ColorPicker, NumberInput, TreeView, DateRangePicker\n- Layout 컴포넌트: FlexLayout, GridLayout, ScrollLayout, StickyLayout, Spacer, Container, AspectRatio\n- HtmlEditor: TinyMCE 기반 HTML 에디터 컴포넌트\n- Icon 컴포넌트: Font Awesome 6.4.x Free 아이콘 지원 (Solid 1,390개 / Regular 163개 / Brands 472개)\n- Alert, EmptyState, LoadingSpinner, Skeleton, StatusBadge, CopyButton 유틸리티 컴포넌트\n\n#### 상태 관리\n\n- 전역 상태 (`_global`): 앱 전체 공유, 페이지 이동 시 유지\n- 로컬 상태 (`_local`): 레이아웃 단위 격리\n- 계산된 상태 (`_computed`): 의존성 기반 자동 재계산\n- 폼 자동 바인딩: Form 컴포넌트 `stateKey` 기반 자동 상태 연동\n- setState 핸들러: target(global/local), 함수형 업데이트, 배열 조작 (push/filter/map)\n- 상태 구독: `G7Core.state.subscribe` 기반 반응형 업데이트\n- initGlobal: 전역 상태 초기값 선언\n- 모달 스코프 상태: `$parent._local` 스냅샷 기반 데이터 전달\n\n#### 인증 시스템\n\n- Laravel Sanctum 하이브리드 인증 (세션 + Bearer 토큰)\n- AuthManager: 싱글톤 인증 상태 관리\n- 자동 토큰 갱신: 401 응답 시 자동 리프레시 후 재시도\n- OptionalSanctumMiddleware: 선택적 인증 지원 (비인증 사용자 허용)\n- 로그인/로그아웃 3단계 프로토콜 (토큰 삭제 → 세션 무효화 → Auth::logout)\n- 비밀번호 재설정: 이메일 인증 기반 토큰 발급 및 검증\n- 회원가입: 약관 동의, 이메일 인증 지원\n\n#### 권한 시스템\n\n- Role 기반 권한 관리 (User → Role → Permission 3계층)\n- permission 미들웨어 체인 기반 접근 제어 (FormRequest authorize() 사용 금지)\n- 확장별 권한 자동 등록/해제\n- 역할별 메뉴 접근 제어 (role_menus 피벗 테이블)\n- 슈퍼 관리자 (superadmin) 전체 권한 자동 부여\n\n#### 메뉴 시스템\n\n- 계층형 메뉴 구조 (parent_id 기반)\n- 역할별 메뉴 가시성 제어\n- 모듈 메뉴 자동 등록 (getAdminMenus 인터페이스)\n- 메뉴 순서 관리 (드래그 앤 드롭 SortableList)\n- 다국어 메뉴명 지원 (JSON 배열 형식)\n\n#### 데이터 소스 (Data Sources)\n\n- API 엔드포인트 선언적 정의 (id, endpoint, method, params)\n- loading_strategy: immediate/lazy/manual 3가지 로딩 전략\n- 데이터소스 의존성: depends_on 기반 연쇄 로딩\n- 폴링: poll_interval 기반 주기적 갱신\n- 조건부 로딩: `if` 표현식 기반 활성화\n- transform: 응답 데이터 변환 함수\n- cache_duration: 응답 캐싱\n\n#### 관리자 기능 (Admin)\n\n- 대시보드: 시스템 정보, 통계 위젯, 최근 활동\n- 사용자 관리: CRUD, 역할 할당, 상태 관리 (활성/비활성/차단)\n- 역할 관리: CRUD, 권한 할당, 다국어 역할명\n- 권한 관리: 카테고리별 권한 목록, 역할별 권한 할당\n- 메뉴 관리: 계층형 메뉴 편집, 순서 변경, 역할별 가시성 설정\n- 모듈 관리: 설치/활성화/비활성화/삭제, 업데이트 확인, 상세 정보 모달, changelog 표시\n- 플러그인 관리: 설치/활성화/비활성화/삭제, 설정 UI, 업데이트 확인, changelog 표시\n- 템플릿 관리: 설치/활성화/비활성화/삭제, 업데이트 확인, changelog 표시\n- 환경설정: 사이트 기본 정보, SEO 설정, 메일 설정, 보안 설정, 탭 레이아웃\n- 일정 관리: 스케줄 CRUD, 캘린더 뷰, 카테고리 분류\n- 메일 템플릿 관리: DB 기반 메일 템플릿 CRUD, 변수 치환, 미리보기 기능\n- 메일 발송 로그: 발송 이력 조회, 상태 추적, 상세 정보 모달\n- 시스템 정보: PHP/Laravel/DB 버전, 디스크 사용량, 확장 현황 표시\n- 코어 업데이트: 버전 확인, 업데이트 가이드, changelog 인라인 표시, 백업 생성\n\n#### 사용자 기능 (User/Public)\n\n- 로그인/회원가입/비밀번호 재설정 페이지\n- 마이페이지: 프로필 수정, 비밀번호 변경\n- 통합 검색 기능\n- 게시판 뷰: 목록/상세/작성/수정 (board 모듈 연동)\n- 에러 페이지: 403, 404, 500, 503 커스텀 에러 페이지\n- 점검 모드(maintenance) 페이지\n\n#### 설치 프로그램 (Installer)\n\n- 다단계 웹 설치 마법사 (환경 체크 → DB 설정 → 관리자 생성 → 완료)\n- SSE(Server-Sent Events) 기반 실시간 설치 진행 상태 표시\n- 설치 롤백 기능: 실패 시 자동 복원 (마이그레이션/시더 롤백)\n- 언어 선택 지원 (한국어/영어)\n- 다크 모드 지원\n- 환경 요구사항 자동 검증 (PHP 버전, 확장, 디렉토리 권한)\n\n#### 모듈: sirsoft-board (게시판)\n\n- 게시판 관리: CRUD, 카테고리, 스킨 설정, 권한 설정\n- 게시글 관리: CRUD, 검색, 정렬, 페이지네이션\n- 댓글 시스템: CRUD, 대댓글 (계층형), 답글 알림\n- 신고 시스템: 게시글/댓글 신고, 관리자 처리 (승인/거절)\n- 첨부파일: 이미지/파일 업로드, 다운로드, 인라인 표시\n- 비밀글: 작성자/관리자만 열람 가능\n- 블라인드/복원: 관리자 블라인드 처리 및 복원 기능\n- 카드/갤러리 레이아웃: 다양한 목록 표시 형태 지원\n- 게시판 권한: 읽기/쓰기/댓글/관리 권한 분리\n- 인기글/최신글 위젯\n- SEO 메타 태그 자동 생성\n\n#### 모듈: sirsoft-ecommerce (이커머스)\n\n- 상품 관리: CRUD, 옵션(사이즈/색상), 라벨, SEO 메타, 이미지 갤러리\n- 상품 카테고리: 계층형 카테고리, 순서 관리, TreeView 편집\n- 브랜드 관리: CRUD, 로고, 설명\n- 주문 관리: 주문 목록, 상태 변경, 상세 정보, 주문 타임라인\n- 쿠폰 시스템: 정액/정률 할인, 사용 조건 (최소 금액, 특정 상품), 유효기간, 사용 횟수 제한\n- 배송 정책: 무료/유료/조건부 배송, 지역별 요금 설정\n- 공통 정보 관리: 배송/교환/환불 안내, 판매자 정보\n- 장바구니: 추가/수량 변경/삭제, 옵션별 관리, 품절 상품 알림\n- 체크아웃: 주소 입력 (다음 우편번호 연동), 배송지 저장 체크박스, 결제수단 선택, 쿠폰 적용\n- 주문 완료: 주문 번호, 결제 정보 요약, 장바구니 자동 비우기\n- 상품 상세 페이지: 이미지 갤러리, 옵션 선택, 수량 입력, 장바구니 담기\n- 상품 목록: 필터링 (카테고리/브랜드/가격), 정렬, 페이지네이션, 카드 그리드\n- 위시리스트: 찜하기/해제 기능\n- 상품 검색: 키워드 검색, 카테고리 필터 연동\n\n#### 모듈: sirsoft-page (페이지 관리)\n\n- CMS 페이지: CRUD, 슬러그 기반 URL 매핑\n- 페이지 버전 관리: 버전 이력 조회, 이전 버전 복원\n- 첨부파일: 이미지/파일 업로드 지원\n- 검색 통합: 페이지 내용 통합 검색 지원\n- SEO: 메타 태그, Open Graph 설정\n\n#### 플러그인: sirsoft-daum_postcode (다음 우편번호)\n\n- 다음 우편번호 검색 API 연동\n- 주소 선택 후 폼 자동 입력\n- 체크아웃 배송지 입력 연동\n\n#### 플러그인: sirsoft-tosspayments (토스페이먼츠)\n\n- 토스페이먼츠 결제 API 연동\n- 카드/계좌이체/가상계좌/무통장입금 결제 지원\n- 결제 확인 (confirm) 프로세스\n- 결제 성공/실패 콜백 처리\n- 관리자 설정 UI (API 키 관리)\n\n#### 플러그인: sirsoft-marketing (마케팅 동의)\n\n- 마케팅 동의 관리: 이메일 구독, 마케팅 동의, 제3자 제공 동의\n- MarketingConsent / MarketingConsentHistory 모델 및 서비스\n- MarketingConsentListener 훅 리스너 (회원가입/프로필 연동)\n- 사용자 관리 화면 레이아웃 확장 (마케팅 동의 상세/폼/프로필)\n- 회원가입 폼 마케팅 동의 항목 확장\n- 플러그인 설정 UI\n- 역할 기반 접근 제어 (마케팅 관리자)\n- 다국어 지원 (ko, en)\n\n#### 플러그인: sirsoft-verification (본인인증)\n\n- 휴대폰 인증, 아이핀 인증 등 본인인증 기능 제공\n- 사용자 관리 화면 레이아웃 확장 (본인인증 상세/폼)\n- 역할 기반 접근 제어 (본인인증 관리자)\n- 다국어 지원 (ko, en)\n\n#### 템플릿: sirsoft-admin_basic (관리자)\n\n- 관리자 대시보드 레이아웃\n- 사이드바 네비게이션: 접기/펼치기, 계층형 메뉴, 활성 상태 표시\n- 상단바: 사용자 정보, 알림 벨, 다크 모드 전환 토글\n- 반응형 레이아웃 (데스크톱/태블릿/모바일)\n- 관리자 전용 컴포넌트: DataGrid, CardGrid, SearchBar, 필터 패널 등\n- CRUD 화면 표준 레이아웃 (목록/생성/수정/상세)\n- 모달 기반 상세 보기/수정 기능\n- 토스트 알림 시스템\n- 확인 다이얼로그 (삭제 확인 등)\n- 환경설정 탭 레이아웃\n- 모듈/플러그인/템플릿 관리 화면 (설치/업데이트/상세 모달)\n- 사용자/역할/권한 관리 화면\n- 메뉴 관리 화면 (드래그 앤 드롭 순서 변경)\n- 일정 관리 캘린더 화면\n- 메일 템플릿 관리 화면\n- 메일 발송 로그 화면\n- 시스템 정보 화면\n- 코어 업데이트 가이드/결과 모달\n\n#### 템플릿: sirsoft-basic (사용자)\n\n- 사용자 메인 페이지 레이아웃\n- 헤더/푸터 공통 레이아웃 (반응형)\n- 로그인/회원가입/비밀번호 재설정 화면\n- 마이페이지 레이아웃\n- 게시판 목록/상세/작성/수정 화면\n- 상품 목록/상세 화면\n- 장바구니/체크아웃/주문완료 화면\n- 검색 결과 화면\n- CMS 페이지 표시 화면\n- 에러 페이지 (403, 404, 500, 503)\n- 점검 모드(maintenance) 전용 페이지\n- 반응형 레이아웃 (데스크톱/태블릿/모바일)\n\n#### 다국어 시스템 (i18n)\n\n- 백엔드 다국어: `__()` 함수 기반, `lang/{locale}/*.php` 파일 구조\n- 프론트엔드 다국어: `$t:key` 즉시 평가 바인딩, `$t:defer:key` 지연 평가 바인딩\n- 컴포넌트 다국어: `G7Core.t()` API 제공\n- 모듈 다국어: 모듈별 독립 언어 파일, 네임스페이스 분리 (`__('vendor-module::key')`)\n- 템플릿 다국어: partial 언어 파일 분할 (admin.json, errors.json 등)\n- DB 다국어 필드: JSON 배열 형식 (`{\"ko\": \"...\", \"en\": \"...\"}`)\n- 지원 로케일: 한국어(ko), 영어(en)\n- TranslatableField 트레이트: 모델 다국어 필드 자동 해석\n\n#### 보안 (Security)\n\n- ValidLayoutStructure: JSON 레이아웃 구조 검증 Custom Rule\n- WhitelistedEndpoint: API 엔드포인트 화이트리스트 검증 Custom Rule\n- NoExternalUrls: 외부 URL 차단 검증 Custom Rule\n- ComponentExists: 컴포넌트 존재 여부 검증 Custom Rule\n- CSRF 보호: Laravel 기본 CSRF 토큰 검증\n- XSS 방지: 출력 이스케이프, HTML sanitize 처리\n- SQL Injection 방지: Eloquent ORM, 파라미터 바인딩 사용\n- Rate Limiting: API 요청 제한 미들웨어\n\n#### 메일 시스템\n\n- DB 기반 메일 템플릿: 제목/본문 DB 관리, 변수 치환 (Blade 문법)\n- Notification + Mailable 통합 패턴: BaseNotification 상속으로 보일러플레이트 제거\n- 메일 발송 로그: 발송 이력 DB 기록, 수신자/상태/발송 시간 추적\n- 메일 템플릿 사용자 오버라이드 추적 (user_overrides 필드)\n\n#### 알림 시스템 (Notification)\n\n- BaseNotification: 모든 알림의 베이스 클래스 (via() 보일러플레이트 제거)\n- 알림 채널: 메일, 데이터베이스, 브로드캐스트 지원\n- 실시간 알림: Laravel Reverb (WebSocket) 기반 Broadcasting\n\n#### 스토리지 시스템\n\n- StorageInterface: 모든 파일 저장의 추상화 인터페이스 (Storage::disk() 직접 호출 금지)\n- CoreStorageDriver: 코어 스토리지 구현체\n- 확장별 독립 스토리지 공간 할당\n\n#### 코어 업데이트 시스템\n\n- CoreUpdateService: GitHub API 기반 코어 버전 확인 및 업데이트 감지\n- CoreUpdateController: 업데이트 상태 확인, 가이드 표시 API 엔드포인트\n- CoreBackupHelper: 업데이트 전 코어 파일 백업 생성\n- FilePermissionHelper: 파일/디렉토리 쓰기 권한 사전 검증\n- MaintenanceModePage 미들웨어: 점검 모드 시 전용 페이지 표시\n- 업데이트 가이드 모달: changelog 인라인 표시, 단계별 안내\n\n#### 그누보드7 DevTools\n\n- MCP 서버: 20+ 디버깅 도구 제공\n - 기본 도구: g7-state, g7-actions, g7-cache, g7-diagnose, g7-lifecycle, g7-network, g7-form, g7-expressions, g7-logs\n - 고급 분석: g7-datasources, g7-handlers, g7-events, g7-performance, g7-conditionals, g7-websocket\n - 상태 계층/스타일: g7-renders, g7-state-hierarchy, g7-context-flow, g7-styles, g7-auth, g7-tailwind, g7-layout\n - Phase 8 심화: g7-computed, g7-nested-context, g7-modal-state, g7-sequence, g7-stale-closure, g7-change-detection\n- 브라우저 상태 덤프: 런타임 상태 캡처 및 MCP 서버 전송\n- UI 패널: 브라우저 내 DevTools 패널 (실시간 상태/액션/로그 확인)\n- 페이지네이션: offset/limit 기반 대용량 데이터 조회 지원\n\n#### WYSIWYG 에디터\n\n- 비주얼 레이아웃 에디터: 드래그 앤 드롭 기반 레이아웃 편집\n- PropertyPanel: 컴포넌트 속성 편집 UI\n- 컴포넌트 팔레트: 사용 가능한 컴포넌트 드래그 목록\n- 실시간 미리보기: 편집 즉시 렌더링 결과 확인\n\n#### 성능 최적화\n\n- 레이아웃 캐싱: 파싱된 레이아웃 JSON + 상속 병합 결과 캐시\n- 확장 캐싱: 모듈/플러그인/템플릿 목록 및 메타데이터 캐시\n- 번역 캐싱: 언어 파일 파싱 결과 캐시\n- ETag 지원: API 응답 조건부 캐시 검증 (304 Not Modified)\n- Gzip 압축: API 응답 자동 압축\n- Debounce 액션: 연속 이벤트 디바운스 처리 핸들러\n- Lazy 로딩: 데이터소스 지연 로딩 (스크롤/이벤트 트리거)\n- 순환 참조 방지 메커니즘: 레이아웃 상속 무한 루프 감지\n\n#### 데이터베이스\n\n- 코어 테이블: users, roles, permissions, role_has_permissions, role_menus, menus, settings, mail_templates, mail_send_logs, schedules, modules, plugins, templates, template_layouts, template_layout_versions\n- 모든 컬럼 한국어 comment 필수 규칙 적용\n- down() 메서드 완전 롤백 구현 필수 규칙 적용\n- 다국어 필드 JSON 배열 형식 표준\n- MariaDB 호환성 지원\n- boolean/enum 컬럼 값 설명 comment 포함 규칙\n\n#### 테스트 인프라\n\n- PHPUnit 11.x: 백엔드 단위 테스트 (Unit) 및 기능 테스트 (Feature)\n- Vitest: 프론트엔드 컴포넌트/템플릿 엔진 테스트\n- createLayoutTest(): 레이아웃 JSON 렌더링 테스트 유틸리티 (Vitest + jsdom)\n- mockApi(): API 응답 모킹 유틸리티 (fetch 자동 모킹)\n- 트러블슈팅 회귀 테스트: 해결된 사례별 자동 검증\n- 테스트 커버리지: 모델, 서비스, 컨트롤러, 컴포넌트, 레이아웃 렌더링\n\n#### 빌드 시스템\n\n- Vite 기반 프론트엔드 빌드 (코드 분할, 트리 쉐이킹)\n- Artisan 커맨드 기반 빌드 관리:\n - `core:build`: 코어 템플릿 엔진 빌드 (--full, --watch 옵션)\n - `module:build`: 모듈 프론트엔드 빌드 (--all, --watch, --active 옵션)\n - `template:build`: 템플릿 빌드 (--all, --watch, --active 옵션)\n - `plugin:build`: 플러그인 빌드 (--all, --watch, --active 옵션)\n- _bundled 디렉토리 기본 빌드, --active 옵션으로 활성 디렉토리 빌드\n- --watch 모드: 파일 감시 기반 실시간 빌드 (활성 디렉토리 자동 사용)\n\n#### Artisan 커맨드\n\n- 모듈 관리: module:list, module:install, module:activate, module:deactivate, module:uninstall, module:composer-install, module:cache-clear, module:seed, module:check-updates, module:update, module:build\n- 플러그인 관리: plugin:list, plugin:install, plugin:activate, plugin:deactivate, plugin:uninstall, plugin:composer-install, plugin:cache-clear, plugin:seed, plugin:check-updates, plugin:update, plugin:build\n- 템플릿 관리: template:list, template:install, template:activate, template:deactivate, template:uninstall, template:cache-clear, template:check-updates, template:update, template:build\n- 확장 공통: extension:composer-install, extension:update-autoload\n- 코어: core:build\n\n#### 개발 도구 및 문서\n\n- AI 에이전트 개발 가이드 (AGENTS.md): 핵심 원칙, 코딩 규칙, 디버깅 프로토콜\n- 규정 문서 체계 (docs/): 백엔드 14개, 프론트엔드 59개, 확장 23개, 공통 4개 (총 100개)\n- AI 에이전트 자동화 도구: 검증/분석/구현 스킬 30+개, 인덱스 자동 생성 스크립트\n- 트러블슈팅 가이드: 상태 관리, 캐시, 컴포넌트, 백엔드 문제 해결 사례집\n- 코드 스타일: Laravel Pint (PSR-12) 자동 적용\n" + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/core-update.md b/docs/backend/api/core-update.md index cdc5e984..fda80651 100644 --- a/docs/backend/api/core-update.md +++ b/docs/backend/api/core-update.md @@ -33,6 +33,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.extension.changelog_rules`). +**요청 예시** + +```http +GET /api/admin/core-update/changelog?source=active&from_version=%EC%98%88%EC%8B%9C%EA%B0%92&to_version=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -41,6 +50,1532 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | changelog | array | `[{"version":"7.0.2","date":"2026-07-05","categories":[{"n…` | 변경 이력 텍스트 (원격/파일 CHANGELOG 본문) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "설정을 성공적으로 가져왔습니다.", + "data": { + "changelog": [ + { + "version": "7.0.2", + "date": "2026-07-05", + "categories": [ + { + "name": "Added", + "items": [ + "설치된 모듈·플러그인이 늘어날수록 화면마다 개별로 내려받던 확장 스크립트·스타일 요청을 종류별로 하나로 묶어 내려받도록 개선해, 확장이 많은 사이트의 초기 로딩 요청 수를 크게 줄였습니다. 확장을 설치·활성화·비활성화하면 묶음이 자동으로 다시 만들어져 항상 현재 활성 상태와 일치합니다." + ] + }, + { + "name": "Changed", + "items": [ + "화면·데이터를 내려받을 때의 전송 용량을 줄여 초기 로딩과 페이지 이동 속도를 개선했습니다.", + "모듈·플러그인의 묶음 스크립트·스타일이 지금까지 압축 없이 전송되던 것을 압축 전송하도록 바꿔, 확장이 많은 사이트에서 첫 화면에 받는 용량을 크게 줄였습니다.", + "한글 등 다국어 텍스트가 응답에 불필요하게 부풀려 담기던 것을 원래 형태 그대로 담도록 바꿔, 특히 다국어 번역 데이터의 전송 용량을 줄였습니다.", + "화면을 그리는 데 쓰이지 않는 개발용 설명 주석을 화면 정보 응답에서 빼도록 해 응답 용량을 추가로 줄였습니다." + ] + }, + { + "name": "Fixed", + "items": [ + "코어 업데이트 시 레이아웃 편집기·개발자 진단도구 번들의 빌드 설정 파일이 갱신 대상에서 누락되던 문제를 수정했습니다. 이제 코어를 업데이트하면 이 파일들도 최신 내용으로 반영됩니다.", + "설치 마법사에서 Vendor 설치 방식을 \"Composer 실행\"으로 선택해 설치할 때, 일부 Windows 환경에서 Composer 설치 로그의 문자 인코딩이 어긋나면 진행 상태 응답이 비어 진행 상황이 표시되지 않고 오류가 반복될 수 있던 문제를 보완했습니다. 어떤 문자가 섞이더라도 진행 로그가 항상 정상적으로 전달되도록 처리했습니다. (#62 @kitrio 님께서 제보해주셨습니다.)", + "설치 마법사의 \"설치 환경 확인\" 단계에서, storage 폴더 하위에 파일이 많이 쌓인 환경일수록 요구사항 검사가 오래 걸리던 문제를 개선했습니다. 폴더 쓰기 권한 검사가 불필요하게 깊은 하위까지 훑던 것을 필요한 범위로 제한해 이 단계가 즉시 완료되도록 했습니다.", + "설치 시 \"읽기 전용 데이터베이스(Read DB)를 사용하지 않음\"으로 선택했는데도, 읽기 전용 DB 접속 정보를 확인하려다 설치 마지막 단계에서 실패하던 문제를 수정했습니다. 이제 읽기 전용 DB를 사용하지 않으면 쓰기 DB 정보만으로 설치가 정상 완료됩니다. 아울러 설치 이후에도 읽기 전용 DB 접속 정보를 비워 두면 조회 연결이 자동으로 쓰기 DB로 대체되도록 하여, 별도의 읽기 전용 DB 없이도 안정적으로 동작합니다. (#63 @kitrio 님께서 제보해주셨습니다.)" + ] + } + ] + }, + { + "version": "7.0.1", + "date": "2026-07-03", + "categories": [ + { + "name": "Changed", + "items": [ + "첫 화면 접속 시 내려받는 코어 스크립트 용량을 절반 이하로 줄여 초기 로딩을 개선했습니다. 레이아웃 편집기와 개발자 진단도구를 코어에서 분리해, 편집기는 편집 화면에 들어갈 때만, 진단도구는 디버그 모드를 켰을 때만 내려받도록 했습니다. 일반 사용자는 평소 쓰지 않는 편집기·진단도구 코드를 더 이상 받지 않습니다.", + "설치가 끝난 사이트에서 매 요청마다 반복되던 불필요한 데이터베이스 조회를 줄여 페이지 응답 속도를 개선했습니다. 알림·본인인증 설정을 준비하는 과정에서 이미 존재가 보장된 테이블을 매번 다시 확인하던 동작을 설치 완료 상태에서는 건너뛰도록 했습니다.", + "화면을 그릴 때마다 서버가 매번 확장 기능의 연결 정보를 처음부터 다시 계산하던 부담을 줄여 페이지 응답 속도를 개선했습니다. 이 연결 정보는 확장을 설치·수정하거나 코어를 업데이트할 때만 바뀌므로 그 시점에 한 번 저장해 두고, 이후 요청에서는 저장된 값을 재사용하도록 했습니다. 확장이나 코어를 변경하면 저장 값이 자동으로 갱신됩니다.", + "설치된 모듈·플러그인의 코드를 찾을 때마다 서버가 파일을 하나하나 탐색하던 부담을 줄여, 특히 공유 호스팅 등 파일 접근이 느린 환경에서 페이지 응답 속도를 개선했습니다. 확장 코드의 위치를 설치·수정 시점에 미리 정리해 두고 이후 요청에서는 그 목록을 바로 참조하도록 했습니다. 확장을 설치·수정·삭제하거나 코어를 업데이트하면 목록이 자동으로 갱신됩니다.", + "코어 업데이트 중 데이터 변환 단계가 현재 환경에서 자동으로 끝까지 실행되지 못하고 중단될 때, 남은 단계를 이어서 실행하는 안내를 실행 권한에 맞게 개선했습니다. 특히 관리자 권한(sudo)으로 업데이트한 경우, 남은 단계를 관리자 권한 그대로 실행하면 그 과정에서 만들어지는 파일이 관리자 소유로 남아 이후 웹사이트가 그 파일에 쓰지 못하는 문제가 생길 수 있습니다. 이제 이런 경우 웹 서버 실행 계정(예: www-data)으로 실행하도록 정확한 명령을 함께 안내하며, 웹 서버 계정을 자동으로 확인하지 못하면 어떤 계정으로 실행해야 하는지 확인 방법을 안내합니다." + ] + }, + { + "name": "Fixed", + "items": [ + "Windows 환경에서 코어 업데이트가 `public/storage` 링크를 복사하는 단계에서 \"디렉토리는 복사할 수 없습니다\" 오류로 중단되던 문제를 수정했습니다. Windows의 저장소 링크(정션)가 이제 링크 그대로 안전하게 보존되어 업데이트가 정상 완료됩니다.", + "별도 데이터 변환 작업이 필요 없는 버전으로 코어를 업데이트할 때, \"실행할 업그레이드 단계가 없다\"는 정상 상황을 실패로 표시하고 중단하던 문제를 수정했습니다. 이제 변환 작업이 없는 업데이트는 매끄럽게 완료되며, 이어서 설치된 확장의 최신 번들 반영 단계까지 정상적으로 진행됩니다.", + "일부 호스팅 환경에서 관리자 환경설정의 보안 화면에 들어갈 때 시스템 정보 조회가 전체 실패(오류 응답)하던 문제를 수정했습니다. 이제 CPU·메모리·디스크 등 특정 항목을 서버 정책상 읽을 수 없더라도 그 항목만 \"알 수 없음\"으로 표시하고 나머지 시스템 정보는 정상적으로 보여줍니다. 읽지 못한 항목은 로그로 원인을 확인할 수 있습니다. (#59 @glitter-gim 님께서 제보해주셨습니다.)" + ] + } + ] + }, + { + "version": "7.0.0", + "date": "2026-07-01", + "categories": [ + { + "name": "Added", + "items": [ + "모듈·플러그인이 기존 화면의 초기화 동작에 자신의 초기화 단계를 덧붙일 수 있도록 레이아웃 확장 기능을 확대했습니다. 화면이 특정 모듈을 직접 알지 않아도, 모듈이 설치되어 있을 때만 그 모듈의 초기화(예: 표시 통화 복원)가 함께 실행됩니다.", + "관리자 대시보드 통계에 설치된 템플릿 수(활성/전체)와 활성 언어팩 수(활성/전체)를 추가했습니다.", + "관리자 대시보드에 최근 발송된 알림 이력을 보여주는 \"최근 알림\" 위젯용 조회 기능을 추가했습니다. 발송 채널·수신자·제목·상태(성공/실패/건너뜀)·발송 시각을 최신순으로 확인할 수 있습니다.", + "본인인증 challenge 취소(cancel) 와 verification 토큰 소비(consume_token) 시점에 외부 본인인증 플러그인이 자기 데이터 정리 로직을 등록할 수 있는 훅 4건 추가.", + "플러그인 개발용 표준 베이스 클래스를 추가하여, 모듈 개발과 동일한 방식으로 Repository · 캐시 · 스토리지 의존성을 선언만으로 자동 주입받을 수 있도록 개선. 플러그인이 코어와 같은 표준 위에서 작성되어 일관된 캐시·파일 격리 동작을 제공합니다.", + "모듈·플러그인이 페이지에 주입하는 레이아웃 확장을 관리자 화면에서 직접 편집할 수 있도록 개선. 편집 내역은 버전으로 기록되어 이전 버전으로 복구할 수 있으며, 저장 전 미리보기를 제공합니다.", + "좌측 트리 — 편집 화면 좌측 목록을 트리 구조로 개편하여 일반 레이아웃과 확장 주입 레이아웃을 구분 표시. 확장 주입 레이아웃은 해당 템플릿에 실제 적용되는 항목만 표시됩니다.", + "편집 진입과 캔버스 — 템플릿 관리 화면에 코드 편집과 레이아웃 편집 진입을 분리하고, 운영 화면 좌측 하단에 편집 권한 보유자 전용 진입 버튼을 추가했습니다. 편집 캔버스는 실제 페이지와 동일한 렌더링 결과를 보여주며, 데스크탑·태블릿·모바일 폭을 즉시 전환해 반응형 결과를 확인할 수 있습니다. 데이터 소스가 정의된 화면은 샘플 데이터로 자동 채워져 실제 API 없이도 화면을 검토할 수 있고, 본문 영역에 사용된 위지윅 에디터 등 동적 자원도 캔버스 안에서 그대로 동작합니다. 좌측 트리에서 화면을 선택하면 주소창이 즉시 반영되어 새로고침·공유·뒤로가기 동작이 자연스럽고, 같은 주소로 직접 진입하면 해당 화면이 자동 선택됩니다. 편집 중 로그인이 풀리면 안내 후 자동으로 로그인 화면으로 이동하며 재로그인 시 편집 화면으로 복귀합니다. 권한 부족·서버 오류·네트워크 끊김 등은 상황별 안내 카드(아이콘·설명·다음 행동 버튼)로 표시합니다.", + "요소 추가·재배치 — 컴포넌트를 클릭해 선택하면 외곽에 + 버튼이 나타나고, + 버튼을 누르면 화면 중앙에 모달 팔레트가 열려 좌측 카테고리 사이드바와 우측 그리드 카드(아이콘 + 라벨 + 유형 뱃지)에서 추가할 컴포넌트를 고를 수 있습니다. 부모 컨테이너에 들어갈 수 있는 컴포넌트만 자동 필터링되며, 검색으로 컴포넌트를 빠르게 찾을 수 있습니다. 추가한 컴포넌트는 템플릿이 정의한 기본 모양·기본 텍스트로 캔버스에 바로 보이며, 추가 직후 해당 위치로 화면이 부드럽게 스크롤됩니다. 컴포넌트 우상단 ⓘ 아이콘으로 속성 설정·복사·삭제 메뉴를 열고, 호버/선택 시 외곽선·8방향 리사이즈 핸들 자리·잠금/네비게이션 어포던스가 표시됩니다. 변경 사항은 Ctrl+Z / Ctrl+Shift+Z 또는 툴바 ↶/↷ 로 되돌리거나 다시 실행할 수 있습니다. 편집 모드에서 컴포넌트를 클릭해도 페이지 이동이 일어나지 않고 선택만 되며, 내부 라우트로 가리키는 요소는 \"→ 이 화면 편집\" 어포던스로 목적지 화면 편집 모드로 즉시 전환됩니다.", + "저장·충돌 방지 — 낙관적 잠금으로 저장 결과는 성공·검증 실패·동시 저장 충돌·네트워크 오류 등 상황별 안내(성공 토스트 / 오류 배너 / 충돌 모달)로 즉시 표시됩니다. 같은 레이아웃을 다른 관리자가 먼저 저장했으면 충돌 안내 모달에서 최신 버전 불러오기를 선택할 수 있고, 신규 추가한 컴포넌트가 그 사이 비활성화된 모듈·플러그인 소속이면 저장이 자동 차단됩니다. 저장 직후에는 되돌리기 이력이 정리되어 저장 시점이 새 기준이 되며, 변경 사항이 있는 상태에서 다른 화면으로 이동하려 하면 저장하지 않은 내용이 있다는 안내가 먼저 표시됩니다. 로그인·회원가입 등 비로그인 전용 화면을 편집할 때는 로그인 상태 미리보기가 적용되지 않아 안내 토스트가 잘못 뜨지 않습니다.", + "속성 편집 — 요소를 선택하면 정렬·글자 크기·굵기·색상·여백·크기 등을 친화적인 컨트롤(분절 버튼·슬라이더·색상 선택·태그 입력)로 조정할 수 있고, 요소 모서리·변을 직접 드래그해 가로·세로 크기를 바꿀 수 있습니다(드래그 크기 조정도 되돌리기/다시 실행에 반영). 반복 영역(목록처럼 같은 틀이 여러 번 펼쳐지는 자리)의 속성을 편집하면 모든 항목에 함께 적용됩니다. 요소를 편집하는 동안에는 선택한 요소만 밝게 유지되고 나머지 화면은 어둡게 잠겨, 다른 요소를 실수로 건드리지 않습니다. 속성 편집 팝업은 헤더를 잡아 옮길 수 있으며 선택한 요소를 가리지 않도록 빈 공간으로 자동 배치되고, 큰 요소에서도 요소 추가·메뉴 버튼이 크기 조절 핸들과 겹치지 않도록 여백을 둡니다. 고급 설정에서 요소를 볼 수 있는 권한을 권한 목록에서 골라 지정할 수 있습니다. 목록 있음·없음, 회원·비회원, 검증 실패·저장 중 같은 화면 상태를 미리보기로 전환해 데이터 의존 영역을 실제와 가깝게 검토할 수 있고, 같은 데이터 이름을 여러 확장이 쓰더라도 각 화면이 자기 출처의 샘플로 올바르게 표시됩니다. 라우트를 전환해도 저장하지 않은 편집 내용이 유지되며, 미저장 상태로 화면을 떠나거나 새로고침하면 경고가 표시됩니다. 마지막 저장 상태로 한 번에 되돌리는 초기화 버튼도 제공합니다.", + "이미지 관리 — 배경 이미지 등 레이아웃 첨부 이미지를 업로드·관리할 수 있으며, 상단 툴바와 배경 이미지 컨트롤에서 썸네일 갤러리를 열어 업로드·삭제·배경 적용을 할 수 있으며, 지정한 배경 이미지가 편집 캔버스에 즉시 반영되고(채움·맞춤·타일 표시 모드 포함) 모달을 다시 열어도 현재 설정이 그대로 복원됩니다. 발행된 배경 이미지는 일반 방문자 화면에서도 정상적으로 표시됩니다.", + "동작·표시조건·정렬 편집 — 요소를 선택하고 속성 편집 팝업에서 (1) 클릭·마우스 오버 시 동작을 \"다른 페이지로 이동\"·\"안내 메시지 보여주기\"·\"화면 상태 바꾸기\"·\"목록 새로고침\"·\"서버에 보내고 결과 처리\"·\"결제 진입\" 같은 친화 명칭으로 조립하고(서버 호출은 성공·실패 후속 동작까지 단계로 구성), (2) 로그인 회원·관리자·데이터 있음/없음·수정/새 화면·입력 오류 등 친화 조건을 \"그리고/또는\"으로 결합해 요소를 보여줄 조건을 정하며, (3) 가로·세로 컨테이너의 자식 배치(방향·정렬·줄바꿈·간격)와 정렬 박스 안 요소의 늘어남·정렬·순서를 시각 컨트롤로 편집할 수 있습니다. 핸들러·코드 용어 없이 친화 입력만 노출되며, 이동할 페이지·데이터 이름은 목록에서 선택합니다. \"결제 진입\" 동작은 특정 결제사에 고정하지 않고, 서버 응답이 알려주는 결제 수단(결제 핸들러·결제 데이터)을 데이터 검색에서 골라 연결합니다 — 결제 수단을 바꾸거나 추가해도 화면 편집 없이 동작합니다.", + "색 모드·디바이스별 스타일 — 속성 편집에 라이트/다크 색 모드와 디바이스별 편집을 더해, 스타일·표시조건 탭 상단의 서브탭에서 라이트/다크 색 모드와 기본값·PC·태블릿·모바일(+ 직접 지정하는 커스텀 화면 폭)을 선택해, 각 범위에 스타일·정렬·표시 조건을 따로 지정할 수 있습니다. 글자색·배경색은 패널의 프리셋 색을 고르면 라이트와 다크 모두에 적용되어 다크 화면용 색을 비개발자도 지정할 수 있고(컬러 피커로 직접 고른 자유 색은 라이트 화면 전용), 어느 범위에 기본값과 다른 설정이 있으면 탭에 표시점이 뜨며 \"기본값으로 초기화\"로 그 범위만 되돌릴 수 있습니다. 편집기 상단 라이트/다크 토글로 미리보기 색상 테마를 전환하면, 관리자 화면이 다크 테마여도 미리보기가 라이트/다크를 정확히 구분해 보여줍니다.", + "화면 문구 다국어 편집 — 상단 툴바의 콘텐츠 언어 선택으로 미리보기에 표시되는 화면 문구의 언어를 전환할 수 있고, 캔버스의 텍스트를 더블클릭하면 그 자리에서 바로 고칠 수 있습니다(Enter 로 확정, Esc 로 취소). 문구를 고치면 그 문구가 다국어 키로 자동 등록되어, 입력한 내용이 즉시 화면에 반영됩니다. 편집 중 떠 있는 서식 막대로 굵게·기울임·밑줄을 켜고 끄거나, 글자 크기·정렬·글자색을 목록에서 골라 적용할 수 있으며(색은 색상 미리보기와 함께 선택), 바뀐 서식은 편집 중에도 즉시 보입니다. 편집 안내 배지를 누르면 속성 편집의 번역 탭이 열려 모든 언어의 값을 한 번에 입력할 수 있어, 한 언어만 고치고 다른 언어 번역을 놓치는 일을 줄입니다. (데이터에 연결된 문구·반복 영역 문구는 직접 편집 대상에서 제외됩니다.)", + "모든 컴포넌트로 속성 편집 확대 — 어떤 요소를 선택해도 속성 탭에 \"요소 ID\" 칸이 항상 제공되어 화면 내 요소에 고유 식별자를 부여할 수 있으며(화면 안전 문자만 입력되고 한글·공백 등은 자동으로 걸러집니다), 컴포넌트별 편집 항목(링크 주소·이미지 주소·입력칸 종류·선택지·정렬 옵션 등)도 친화 컨트롤로 편집할 수 있습니다. 아이콘은 자유 입력 대신 검색 가능한 그리드에서 골라 지정할 수 있어(템플릿이 제공하는 아이콘 목록을 검색으로 빠르게 찾기), 아이콘 이름을 외우지 않아도 됩니다. 입력칸의 선택지처럼 여러 항목으로 된 값은 항목 추가·삭제·순서 변경으로 편집하며, 이미 데이터에 연결된 값은 실수로 덮어쓰지 않도록 보호됩니다.", + "모달 시각 편집 — 좌측 트리에 각 화면이 사용하는 모달 목록이 \"이 화면의 모달\" 그룹으로 표시되고(공통 레이아웃의 모달, 모듈·플러그인 화면의 모달, 별도 파일로 분리된 모달 포함), 각 모달은 \"주문 취소\"·\"비회원 조회 비밀번호 재설정\"처럼 친화 제목으로 표시됩니다. 모달을 선택하면 실제 화면 위에 모달이 열린 상태로 문구 수정·요소 추가·저장을 할 수 있습니다. 모달 바깥 영역은 어둡게 잠겨 클릭·드래그·편집이 차단되어 실수로 호스트 화면을 건드리지 않습니다.", + "버전 배지·복원 — 좌측 트리에 각 화면과 확장 주입 조각의 현재 저장 버전 배지가 표시되고, 버전 기록 모달에서 이전 버전과의 비교(diff)와 복원을 확장 주입 조각에서도 동일하게 사용할 수 있으며, 저장·복원 시 배지가 새로고침 없이 즉시 갱신되고 복원 결과가 캔버스에 바로 반영됩니다.", + "공통 레이아웃 편집 — 각 화면의 콘텐츠가 채워질 슬롯 자리가 점선 박스와 라벨로 표시되고(슬롯 자체는 편집 대상이 아니라 잠금), 헤더·푸터를 클릭해 바로 선택·편집할 수 있습니다. 모달 편집 중에도 편집 캔버스의 스크롤이 그대로 유지됩니다.", + "화면 상태 미리보기 — 모달·공통 레이아웃까지 확대해, 배송지 직접 입력, 모바일 메뉴 펼침, 쿠키 배너 표시, 본인인증 진행 모달처럼 특정 조작에서만 나타나는 영역을 상태 전환만으로 미리보며 편집할 수 있고, 모달 안에 확장이 주입한 영역도 시각 편집할 수 있습니다.", + "일부 템플릿(사용자 템플릿 등)의 화면을 미리볼 때 헤더가 \"데이터를 표시할 수 없습니다\"로 표시되지 않고, 미리보기용 샘플 데이터가 모든 템플릿에서 안전하게 렌더됩니다.", + "회원이 아닌 사용자(비회원)에게도 이메일 알림을 보낼 수 있도록 알림 시스템을 확장했습니다. 회원에게 알림을 보내던 것과 동일한 방식으로, 입력한 이메일을 수신자로 알림이 발송되며 발송 내역에도 함께 기록됩니다. 알림 종류별로 비회원 발송을 허용할 수단을 선택할 수 있어, 이메일은 비회원에게 보내되 사이트 안 알림처럼 로그인이 필요한 수단은 보내지 않도록 구분됩니다(새 알림 수단이 추가될 때 비회원 허용 여부를 별도로 정하지 않으면 기본적으로 보내지 않습니다). 비회원에게 가는 알림은 그 사용자가 보던 화면 언어로 발송됩니다." + ] + }, + { + "name": "Changed", + "items": [ + "모든 번들 플러그인이 위 표준 베이스 클래스로 이관되어 캐시·스토리지 도메인이 코어 및 다른 확장과 명확히 격리되도록 개선. 한 플러그인의 캐시 키가 코어 캐시 영역을 침범하던 잠재 결함을 차단하며, 관리자 화면의 변경사항이 의도대로 즉시 반영됩니다.", + "템플릿이 정의하는 에러 페이지(404 / 403 / 500 / 503 / 401 / 점검) 의 식별자를 다른 모든 페이지와 동일한 디렉토리 접두사 포함 형식(`errors/{코드}`) 으로 통일. 템플릿 설치 시 에러 페이지가 일관된 식별자로 시드되며, 일부 환경에서 에러 페이지가 \"찾을 수 없음\" 응답으로 떨어지던 회귀를 차단합니다. 번들 템플릿(`sirsoft-basic`, `sirsoft-admin_basic`) 의 에러 페이지 설정도 함께 정합화됩니다.", + "템플릿 외부 리소스 위임 — Font Awesome·웹폰트 등 외부 CDN 리소스를 페이지 코드에서 하드코딩하지 않고 템플릿의 `externals` 선언으로 위임. 외부 스타일시트·웹폰트·스크립트와 preconnect·preload 등 리소스 힌트를 한 곳에서 관리하며, 관리자 템플릿과 사용자 템플릿에 동일하게 적용", + "템플릿 컴포넌트 에셋 캐시 버스팅 — `?v=time()` → `?v={{ $extensionCacheVersion }}`로 변경하여 매 요청 무효화 → 확장 변경 시점에만 무효화", + "모듈·플러그인 업데이트 시 관리자가 편집한 확장 레이아웃을 보존하거나 덮어쓰는 전략을 선택할 수 있도록 개선.", + "코어 업그레이드 스텝 명령에 보조 단계(권한 정상화·마이그레이션·캐시 정리 등)를 건너뛰고 스텝만 실행하는 옵션 추가.", + "코어 영역의 프론트엔드 다국어 자원(에러 메시지·시스템 토스트 등) 위치를 정리하여 어떤 템플릿이 부팅되어도 자동 노출되도록 개선. 이전에 코어 에러 화면에서 `core.errors.*` 같은 키가 raw 로 노출되던 결함이 해소됩니다." + ] + }, + { + "name": "Fixed", + "items": [ + "정책 관리에서 코어·모듈·플러그인이 제공한 정책의 \"인증 목적\"·\"적용 대상\"·\"우선순위\"를 바꿔 저장해도 반영되지 않던 문제를 수정했습니다 — 이제 정책 키·강제 시점·강제 위치(정책이 걸리는 지점)만 고정되고, 그 외 모든 항목을 운영자가 자유롭게 변경할 수 있으며, 변경한 값은 확장 업데이트 후에도 유지됩니다.", + "한 화면에 본인인증 정책이 여러 개 걸리고 우선순위가 같을 때, 어느 정책이 먼저 적용될지 정해지지 않아 의도한 인증(예: 성인 인증)이 무시되고 다른 인증이 적용될 수 있던 결함을 수정했습니다. 이제 우선순위가 같아도 항상 동일한 정책이 적용되도록 적용 순서를 고정하고, 정책 추가·수정 화면에 우선순위 입력칸을 제공하며 같은 위치에 같은 우선순위의 활성 정책을 저장하려 하면 저장을 막고 안내합니다. 정책 목록에도 우선순위 열을 추가해 한눈에 확인할 수 있습니다.", + "성인 인증 같은 추가 조건을 함께 요구할 때, 그 조건을 충족하지 못해 인증이 실패하면 실패 사유 안내와 \"본인 확인이 필요합니다\" 안내가 동시에 뜨던 문제를 수정했습니다. 이제 추가 조건 미충족으로 실패하면 그 사유 안내 하나만 표시되며, 일반 본인인증 실패의 안내는 종전대로 표시됩니다.", + "환경설정의 기본 프로바이더 / 목적별 프로바이더 / 코드 유효시간 / 최대 시도 횟수 4개 항목이 admin UI 저장값을 무시하던 결함 수정. 변경한 환경설정값이 회원가입·비밀번호 재설정 등 모든 본인인증 흐름에 즉시 반영됩니다.", + "환경설정 > 본인인증 > 목적별 프로바이더에서 일부 본인인증 목적(예: 성인인증)의 프로바이더를 지정해 저장하면 \"설정 저장에 실패했습니다\" 오류로 전혀 저장되지 않던 결함 수정. 이제 모든 본인인증 목적의 프로바이더 매핑이 정상 저장됩니다. 아울러 저장 검증에 실패하면 문제가 된 목적별 프로바이더 입력칸이 빨갛게 강조되고 그 아래에 사유가 표시되어, 어느 항목을 고쳐야 하는지 바로 확인할 수 있습니다.", + "정책에 프로바이더가 지정되지 않은 경우 환경설정의 기본 프로바이더가 자동 적용되도록 보강. 운영자가 정책마다 프로바이더를 일일이 지정하지 않아도 환경설정 기본값으로 일관되게 동작합니다.", + "코드 입력 모달의 \"남은 시도 횟수\" 표시가 환경설정값과 무관하게 항상 5회로 굳어 있던 결함 수정. 운영자가 환경설정에서 지정한 한도가 사용자 화면에 정확히 표시됩니다.", + "인증 코드를 최대 횟수까지 잘못 입력해 시도가 소진되었을 때, 화면에 \"최대 시도 초과 — 다시 요청해주세요\" 안내 대신 일반 오류 문구(\"인증 코드가 올바르지 않습니다\")가 표시되던 결함 수정. 시도가 소진되면 재요청을 안내하는 메시지가 정확히 표시됩니다.", + "본인인증을 사용하는 모듈·플러그인을 비활성화해도 해당 확장이 설정한 본인인증 요구가 계속 적용되던 결함 수정. 이제 확장을 비활성화하면 그 확장이 선언한 본인인증 정책도 함께 적용 해제되고, 다시 활성화하면 운영자가 조정한 설정을 유지한 채 정상 복구됩니다.", + "확장 메뉴 동기화 시점에 운영자가 수정하지 않은 필드 (아이콘·이름·정렬·URL) 가 잘못 \"사용자 수정\" 으로 표시되어 이후 모듈/플러그인 정의값으로의 자동 동기화가 영구히 차단되던 결함 수정. 확장 재설치·업데이트 시 메뉴 정의 변경이 정상 반영됩니다.", + "확장 역할 동기화 시점에 운영자가 수정하지 않은 필드 (역할 이름·설명) 가 잘못 \"사용자 수정\" 으로 표시되어 이후 모듈/플러그인 정의값으로의 자동 동기화가 영구히 차단되던 결함 수정. 메뉴 동기화와 동일 맥락으로, 확장 재설치·업데이트 시 역할 정의 변경이 정상 반영됩니다.", + "모듈·플러그인의 레이아웃 확장이 대상이 아닌 템플릿(관리자 확장 ↔ 사용자 템플릿)에도 등록되던 결함 수정. 업그레이드 시 잘못 등록된 항목을 자동 정리하며, 이후로는 대상 레이아웃이 존재하는 템플릿에만 등록됩니다.", + "모듈·플러그인이 공통 레이아웃(헤더·푸터 등 모든 화면이 공유하는 틀)에 주입한 UI·초기화 동작이 실제 화면에는 전혀 나타나지 않던 결함 수정. 개별 화면이 공통 레이아웃을 물려받을 때 그 \"물려받은 출처\" 정보가 사라져, 공통 레이아웃을 대상으로 한 확장 주입이 어느 화면에도 적용되지 않았습니다. 이제 화면이 물려받은 공통 레이아웃을 대상으로 한 확장도 정상 적용되어, 모듈이 설치되어 있으면 해당 UI(예: 헤더 통화 선택기)와 초기화 동작이 모든 관련 화면에 표시됩니다.", + "확장(모듈·플러그인) 변경 후 라우트 트리·메뉴·다국어 응답이 옛 값으로 남아 있던 결함 수정. 이전에는 클라이언트가 캐시 버전 신호를 동반하지 않아 백엔드 캐시가 갱신 시점을 놓쳤습니다. 확장 설치/활성화/업데이트가 일어나면 다음 페이지 로드부터 새 값이 즉시 보입니다.", + "확장을 업데이트하거나 레이아웃을 저장해도 변경 내용이 화면에 반영되지 않고 옛 내용이 계속 보이던 결함 수정. 캐시 갱신 신호가 기록되는 저장소와 읽히는 저장소가 환경설정에 따라 어긋날 수 있어, 올린 갱신 신호가 화면 쪽에서 보이지 않던 문제였습니다. 이제 갱신 신호가 항상 같은 저장소로 일관되게 기록·조회되어 확장 업데이트·레이아웃 변경이 다음 화면 로드부터 즉시 반영됩니다.", + "코어 업데이트 시 사용자가 모듈·플러그인·템플릿·언어팩의 `_bundled` 폴더 아래에 직접 만들어 둔 커스텀 폴더·파일이 함께 삭제되던 결함 수정. 이제 코어 업데이트는 코어가 제공하는 번들 확장만 갱신하고, 사용자가 추가한 항목은 그대로 보존합니다. (#43 @bigmsg 님께서 제보해주셨습니다.)", + "관리자 권한(`sudo`)으로 코어를 업데이트한 뒤, 모듈·플러그인·템플릿·언어팩을 업데이트하려 하면 \"백업 생성 중\" 단계에서 `mkdir(): Permission denied` 오류로 실패하던 결함 수정. 관리자 권한 업데이트가 백업 폴더와 업그레이드 기록 파일을 관리자 소유로 남겨, 이후 웹서버 계정이 같은 위치에 쓰지 못하던 문제였습니다. 이제 이들 폴더·파일의 소유권과 그룹 쓰기 권한이 정상화되고, 코어와 확장의 업그레이드 기록을 별도 파일로 분리해 두 작업이 서로의 기록 파일을 두고 충돌하지 않습니다.", + "회원에게 보내는 알림(이메일·사이트 알림)이 보내는 사람(관리자 등)의 화면 언어가 아니라 받는 회원 본인의 언어로 발송되도록 수정. 관리자가 한국어 화면에서 다른 언어를 쓰는 회원의 주문 상태를 바꿔도, 알림은 그 회원의 언어로 전달됩니다.", + "신규 주문 접수 시 관리자에게 발송되는 알림 메일의 결제금액이 0원으로 표시되던 결함 수정. 결제 예정 금액이 정확히 표시됩니다.", + "알림 템플릿을 기본값으로 복원할 때 제목·본문만 복원되고 수신자·클릭 주소는 관리자가 수정한 값이 그대로 남던 결함 수정. 이제 복원 시 수신자·클릭 주소까지 기본값으로 정확히 되돌아갑니다.", + "설치 마법사에서 데이터베이스 테이블 접두사를 지나치게 길게 입력하면 일부 확장 설치 시 테이블 인덱스 이름이 데이터베이스 한도를 넘겨 설치가 실패할 수 있던 문제를 예방합니다 — 접두사 입력 길이를 6자로 제한하고, 한도를 넘기면 설치 전 단계에서 안내합니다.", + "인스톨러에서 \"기존 테이블 삭제 후 설치\"를 선택할 때, 입력한 테이블 접두사와 무관한 다른 테이블까지 모두 삭제되던 결함 수정. 이제 입력한 접두사로 시작하는 테이블만 삭제되어, 같은 데이터베이스를 다른 프로그램과 함께 쓰거나 다른 접두사로 재설치할 때 무관한 테이블이 보존됩니다.", + "관리자 대시보드의 최근 활동·모듈/플러그인/템플릿 상태·게시판/이커머스 위젯 등 같은 틀이 여러 번 펼쳐지는 목록에서, 항목마다 고유 식별자를 붙이도록 작성된 화면이 모든 행에 같은 식별자를 그대로 내보내 일부 보조 기능·접근성 도구가 항목을 구분하지 못하고 일부 항목이 화면에서 누락되어 보이던 결함을 수정했습니다 — 이제 반복 목록의 각 행이 고유하게 식별되어 모든 항목이 정상 표시됩니다.", + "한 화면 안에서 탭이나 필터를 클릭으로 전환할 때, 화면에 따라 표시 조건에 묶인 데이터 목록이 비어 보이던 결함 수정. 새로고침해야 보이던 목록이 이제 탭·필터 전환 즉시 정상 표시됩니다. (표시 조건에 따라 로딩되는 데이터 목록이 전환 시점에 다시 평가되지 않던 문제였습니다.)", + "환경설정 화면 등에서 별도 표시 텍스트 없이 켜고 끄는 토글·체크박스를 켜서 저장하면 \"참/거짓 값이어야 합니다\" 오류로 저장이 거부되던 결함 수정. 토글의 켜짐/꺼짐 상태가 문자열이 아닌 참/거짓 값으로 정확히 전달되어 정상적으로 저장됩니다.", + "관리자 목록 화면에서 필터(발급상태·사용여부 등)를 선택하고 검색하면 일부 필터가 검색 직후 \"전체\"로 풀려 보이던 결함 수정. 새로고침해야 선택한 필터가 다시 표시되던 불편을 없애고, 검색 직후에도 선택한 필터가 그대로 유지됩니다.", + "검색창에 `<`, `>`, `(`, `+` 등 특수문자를 입력하면 오류가 발생하던 결함 수정. 특수문자는 일반 검색어로 처리되어, 어떤 문자를 입력해도 오류 없이 검색 결과(또는 빈 결과)가 표시됩니다.", + "IME(한글·일본어·중국어) 입력 도중 Enter 로 글자를 조합 확정할 때 마지막 글자가 누락되거나 검색·제출이 두 번 실행되던 결함 수정. 이제 조합이 끝난 뒤의 Enter 만 동작합니다. (#54 @devrhee16 님께서 제보해주셨습니다.)", + "관리자 대시보드의 시스템 리소스(CPU·메모리·디스크) 수집이 일부 호스팅 환경(open_basedir 제한, 일부 시스템 함수 비활성)에서 실패할 때 대시보드 리소스 조회 전체가 오류로 응답하던 결함 수정. 이제 수집할 수 없는 항목만 \"알 수 없음\"으로 표시되고 나머지 화면은 정상 동작합니다. (#40 @glitter-gim 님께서 제보해주셨습니다.)", + "시스템 정보 화면의 디스크 사용량 조회도 동일한 환경에서 안전하게 \"알 수 없음\"으로 표시되도록 보강했습니다.", + "웹소켓 사용을 끈 상태에서도 실시간 연결이 일부 동작할 수 있던 문제 수정. 이제 웹소켓 사용을 끄면 실시간 연결·채널 인증이 완전히 비활성화됩니다. (#50 @bigmsg 님께서 제보해주셨습니다.)", + "사이트 이름을 언어별로 다르게 저장한 환경에서 브라우저 탭 제목과 검색 메타데이터(JSON-LD)가 올바르게 표시되지 않거나 페이지가 깨질 수 있던 문제 수정. 이제 모든 메타 경로가 현재 언어의 사이트 이름을 일관되게 사용합니다. (#49 @glitter-gim 님께서 제보해주셨습니다.)", + "존재하지 않는 페이지에 접근할 때 정상 페이지처럼 200 응답을 반환하던 문제 수정. 이제 미등록 경로는 올바른 404 응답을 반환해 검색엔진이 없는 페이지를 색인하지 않습니다. (#47 @glitter-gim 님께서 제보해주셨습니다.)", + "인증이 필요한 관리자 API에 로그인 없이 접근할 때, 브라우저 주소창 직접 입력 등 일부 요청에서 서버 오류로 응답하던 결함 수정. 이제 어떤 방식으로 접근해도 일관되게 \"인증 필요\" 응답이 반환됩니다. (#39 @glitter-gim 님께서 제보해주셨습니다.)" + ] + }, + { + "name": "Security", + "items": [ + "사용되지 않는 파일 서빙 엔드포인트(`/storage` 직접 업로드·다운로드 경로) 노출을 제거했습니다. 실제 파일 업로드·다운로드는 별도의 인증된 경로로 처리되며 영향이 없습니다. (#52 @glitter-gim 님께서 제보해주셨습니다.)", + "설치가 완료된 시스템에서 설치 상태 관리 엔드포인트(`/install/api/state-management.php`)가 다른 설치 API 와 동일하게 차단되지 않아, 설치 완료 후에도 설치 상태 조회·초기화·중단 요청이 가능하던 문제를 수정했습니다. 설치가 완전히 끝난 환경에서는 해당 요청이 모두 HTTP 410 으로 차단됩니다. 설치 진행 중 완료 화면으로 정상 전환되는 동작은 영향받지 않습니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1056)" + ] + }, + { + "name": "Removed", + "items": [ + "기본 제공 결제 수단을 KG이니시스로 단일화했습니다. 그 외 결제 연동(나이스페이먼츠·NHN KCP·토스페이먼츠)은 기본 번들에서 제외되며, 필요한 경우 별도 플러그인으로 설치해 사용할 수 있습니다. 이미 해당 결제 연동을 설치해 사용 중인 사이트는 영향받지 않습니다." + ] + } + ] + }, + { + "version": "7.0.0-beta.7", + "date": "2026-05-15", + "categories": [ + { + "name": "Fixed", + "items": [ + "인스톨러 설치 완료 후 `.env` 에 DB 자격증명과 `APP_KEY` 가 기록되지 않고 `storage/installer/runtime.php` 에 평문으로 잔존하던 결함을 수정. 운영 동작은 정상이었으나 자격증명이 정상적인 위치로 옮겨지지 않고 임시 파일에 보존되는 상태였습니다. (#37 @glitter-gim 님께서 제보해주셨습니다.)", + "beta.4~beta.6 출시본에서 위 결함으로 인해 자격증명이 잔존한 환경을 beta.7 업그레이드 시 자동으로 감지하여 `.env` 로 머지하고 임시 파일을 정리하도록 업그레이드 스텝을 추가했습니다. `.env` 의 `INSTALLER_COMPLETED` 마커가 이미 설정된 정합 이상 케이스는 자동 정리 대상에서 제외하고 운영자 수동 검토를 안내합니다.", + "위 업그레이드 스텝이 `.env` 를 머지할 때 권한·소유자·그룹을 웹 서버 기준으로 정렬하도록 보강. 기존 `.env` 가 있던 환경에서는 운영자가 설정한 권한을 그대로 보존하고, 새로 생성되는 환경에서는 웹 서버가 읽을 수 있는 권한과 그룹을 자동으로 적용합니다. 명령줄 업그레이드 실행 사용자와 웹 서버 사용자가 다른 자체 구축 서버 환경에서 머지 직후 웹 서버가 `.env` 를 읽지 못해 사이트가 다운되는 회귀를 차단합니다.", + "인스톨러 설치 완료 화면의 `.env` 권한 강화 안내(`chmod`/`chgrp` 명령 예시) 가 Windows 환경에서 비노출되도록 OS 가드 추가. POSIX 권한 모델이 없는 Windows 환경에서 무의미한 Linux/macOS 전용 명령이 노출되어 운영자 혼란을 유발하던 문제를 수정합니다.", + "beta.4~6 출시본의 인스톨러 결함으로 자격증명이 `storage/installer/runtime.php` 에 잔존한 환경에서 beta.7 업그레이드 시 번들 확장 일괄 업데이트 단계가 `Access denied for user 'root'@'localhost'` 로 실패하던 회귀 수정. 업그레이드 스텝이 `.env` 머지 직후 부모 프로세스의 환경 변수도 함께 갱신하여 후속 자식 프로세스가 정상 자격증명을 상속받도록 보강합니다. 정상 설치 환경 (beta.3 등) 에서는 트리거되지 않습니다.", + "위 동일 회귀의 추가 차단 — 코어 업데이트가 별도 프로세스로 업그레이드 스텝을 실행하는 경로에서는 자식 종료 후 부모의 환경 변수가 갱신되지 않아 동일 증상이 재발하던 결함 수정. 자식 프로세스가 부팅 시점에 디스크의 `.env` 와 자신의 환경 변수를 대조하여 손상이 감지되면 디스크 값으로 보정하도록 보강하여, 다음 단계의 확장 일괄 업데이트가 정상 자격증명으로 진행되도록 합니다. 정상 설치 환경에서는 자동 비활성화됩니다." + ] + }, + { + "name": "Changed", + "items": [ + "`INSTALL.md` 에 \"업그레이드\" 섹션을 신설하여 자동 업데이트의 표준 절차, `sudo` 권장 사유 (`.env` · `bootstrap/cache` 의 권한 모델), 사용자군별 단계적 업그레이드 분기, 권한·캐시 트러블슈팅을 한곳에서 안내하도록 보완. 관리자 환경설정 > 정보 > 업데이트 확인 모달이 본 섹션으로 외부 링크를 노출합니다. (#30 @yks118 님께서 건의해주셨습니다.)", + "관리자 환경설정 > 정보 > 업데이트 확인 모달의 자동 업데이트 명령어 안내를 `sudo php artisan core:update` 권장 형태로 변경. 권한이 일치하지 않는 환경(`.env` 가 웹 서버 사용자 소유) 에서의 실행 실패를 사용자가 사전에 회피할 수 있도록 모달 하단에 \"전체 업그레이드 가이드 보기\" 외부 링크를 추가해 INSTALL.md 의 업그레이드 섹션으로 연결합니다. (#30 @yks118 님께서 제보해주셨습니다.)" + ] + }, + { + "name": "Notes — `.env` 권한 권장안", + "items": [ + "가장 안전한 권한은 `0600` (소유자만 읽기·쓰기) 입니다. `.env` 는 DB 비밀번호와 `APP_KEY` 등 평문 자격증명을 포함하므로 외부 접근을 최소화하는 것이 권장됩니다.", + "단, `0600` 적용 전에 **`.env` 의 소유자가 웹 서버 실행 사용자와 동일해야** 합니다 (예: 단일 사용자 환경의 `chown www-data:www-data .env && chmod 600 .env`). 소유자가 웹 서버 사용자와 다른데 `0600` 으로 변경하면 웹 서버가 `.env` 를 읽지 못해 사이트가 응답하지 않습니다.", + "명령줄 사용자와 웹 서버 사용자가 다른 환경 (자체 구축 서버 등) 에서는 `0640` 권한 + 그룹을 웹 서버 그룹으로 설정 (`chgrp www-data .env && chmod 640 .env`) 하는 것이 안전한 절충안입니다. 웹 서버는 그룹 멤버로 읽고, 명령줄 사용자는 직접 수정할 수 있습니다." + ] + } + ] + }, + { + "version": "7.0.0-beta.6", + "date": "2026-05-14", + "categories": [ + { + "name": "Fixed", + "items": [ + "코어 업데이트 자동 롤백 시 신 버전이 추가한 신규 파일이 활성 디렉토리에 잔존하여 `BindingResolutionException` 등 부정합 부팅 실패가 발생하던 결함 수정. 백업 디렉토리에 신규 파일 manifest 를 기록하고 롤백 시 정확히 그 목록만 정리합니다. 사용자가 코어 영역에 직접 추가한 파일과 모든 symlink (`public/storage` 등) 는 보존됩니다. (#34 @bigmsg 님께서 제보해주셨습니다.)", + "자동 롤백 후 `bootstrap/cache` 잔존 PHP 캐시로 인한 추가 부팅 실패 차단 — 롤백 직후 캐시를 자동으로 정리합니다.", + "beta.5 이전 자동 롤백으로 인해 활성 디렉토리에 잔존했을 수 있는 ServiceProvider 후보를 beta.6 업그레이드 스텝이 진단 로그로 식별하여 운영자 수동 검토를 안내합니다. 자동 삭제하지 않으므로 사용자가 직접 추가한 ServiceProvider 는 보존됩니다.", + "업그레이드 스텝 단독 실행 명령(`core:execute-upgrade-steps`) 이 마이그레이션·코어 재동기화·버전 갱신·캐시 정리·번들 확장 일괄 업데이트를 자동으로 함께 수행하도록 보강. 자동 업데이트 중단 후 안내된 수동 명령을 그대로 실행하면 별도 보조 작업 없이 업그레이드가 완료됩니다.", + "인스톨러 Step 3 (PHP CLI / Composer / 자동 감지 / 코어 _pending 경로 검증) 의 프론트엔드 catch 분기에서 사용하는 다국어 키가 언어 파일에 정의되어 있지 않아, 서버 응답 실패 시 화면에 다국어 키 문자열이 그대로 노출되던 결함 수정.", + "인스톨러 다국어 파일에 중복 정의된 키 4건 정리. POST 전용 API 의 메서드 거부 메시지가 일반 문구로 덮여 표시되던 문제, 데이터베이스 연결 실패 로그에서 placeholder 가 치환되지 않던 문제, HTTPS 카드 라벨이 안내 문장으로 표시되던 문제가 함께 해결됩니다.", + "인스톨러 Step 3 의 PHP CLI 및 Composer 절대경로 검증이 공유 호스팅 환경(시놀로지 DSM 등 `open_basedir` 가 시스템 binary 영역을 차단하는 환경)과 Windows 환경에서 정상 경로임에도 거부되던 결함 수정. 셸 인자 escape 와 메타문자 차단은 유지하고, 파일 시스템 권한에 의존하던 사전 검사를 실제 실행 결과로 판정하도록 변경합니다. Windows 의 백슬래시 경로 구분자도 정상 입력으로 인식됩니다 (단, `C:\\Program Files\\...` 같은 공백 포함 디렉토리는 공백 없는 경로 또는 8.3 short path 사용 권장). (#33 @glitter-gim 님께서 제보해주셨습니다.)", + "코어 운영 단계의 composer 바이너리 자동 인식이 같은 `open_basedir` 환경에서 실패하여 모듈/플러그인 설치 시 vendor-bundle 모드만 사용 가능하던 결함 수정. composer 가 정상 설치되어 PATH 에 존재하는 환경에서도 인식되도록 검출 로직을 개선했습니다.", + "멀티 PHP 버전 환경(시놀로지 DSM Web Station, cPanel/Plesk multi-PHP 등) 에서 Composer 를 특정 PHP 인터프리터로 실행하려는 \"PHP 절대경로 + Composer 절대경로\" 공백 분리 입력 형식이 거부되던 결함 수정. 입력을 두 토큰으로 분리한 뒤 각 토큰을 개별 escape 처리하여 안전을 유지하면서 멀티 PHP 운영 시나리오를 지원합니다.", + "인스톨러 세션 쿠키가 일부 환경(비표준 포트 + dynamic DNS 도메인 + 브라우저 추적 보호 등) 에서 브라우저에 의해 차단되어 첫 화면 \"설치하기\" 클릭이 동작하지 않고 새로고침처럼 보이던 결함을 보완. 세션 쿠키 SameSite 정책을 Strict → Lax 로 완화하여 차단 케이스 자체를 줄이고, Step 0 진입 시 쿠키 round-trip 사전 진단을 수행하여 차단이 감지되면 사용자에게 회피 방법(다른 브라우저 / 쿠키 차단 해제 / IP·localhost 접속) 을 명시 안내합니다." + ] + }, + { + "name": "Added", + "items": [ + "`hotfix:rollback-stale-files` Artisan 커맨드 추가 — 자동 롤백 후 활성 디렉토리에 잔존할 수 있는 신 파일을 운영자가 명시 실행하여 진단/정리할 수 있는 단발성 회복 도구. 기본은 진단 모드 (실제 삭제 없음), `--prune` 옵션 시 확인 프롬프트 후 정리. symlink 와 보호 경로(`storage/`, `vendor/`, `.env*` 등) 는 자동 제외.", + "본인인증 메시지 정의 목록 API 응답에 `can_create` 권한 키 추가 — 운영자의 수정 권한 보유 여부에 따라 관리자 화면이 \"정의 추가\" 버튼을 정확히 활성화/비활성화할 수 있도록 보강." + ] + } + ] + }, + { + "version": "7.0.0-beta.5", + "date": "2026-05-12", + "categories": [ + { + "name": "Upgrade Notice", + "items": [ + "손실 항목 1: 운영자가 활성 디렉토리(`modules/{id}`, `plugins/{id}`, `templates/{id}`, `lang-packs/{id}`)에서 직접 수정한 코드", + "손실 항목 2: 외부 install 한 확장 (GitHub 등 `_bundled` 에 포함되지 않은 모듈/플러그인/템플릿/언어팩)", + "`modules/`", + "`plugins/`", + "`templates/`", + "`lang-packs/`", + "**사용자군 B — 정상 beta.4 사용자 (대다수)**: `php artisan core:update` 표준 절차. beta.5 의 자동 복구 스텝이 활성 확장 디렉토리 + lang-packs 정합성을 보장합니다.", + "**사용자군 A — beta.3 → beta.4 업그레이드 도중 `Call to undefined method` 류 fatal 후 stuck 한 사용자**: `.env` `APP_VERSION` 을 변경하지 마시고 그대로 `php artisan core:update --force` 를 재실행하면 됩니다. 디스크는 이미 beta.4 이므로 새 자식 프로세스가 beta.5 메모리로 부팅되어 남은 업그레이드 스텝이 멱등 적용됩니다. 단, 활성 디렉토리에서 직접 수정한 코드 또는 `_bundled` 에 없는 외부 확장은 자동 복구 대상이 아니므로 업그레이드 전 위 디렉토리 백업이 동일하게 권장됩니다.", + "**사용자군 C — beta.1 또는 beta.2 사용자**: `php artisan core:update` 1회 호출로 beta.5 까지 직접 업그레이드는 권장하지 않습니다. 중간 버전의 업그레이드 스텝이 이전 버전 메모리에 없는 신규 코드에 의존하여 fatal 위험이 있습니다. 다음 단계적 업그레이드를 권장합니다:", + "감지 조건: `public/storage` 가 일반 디렉토리 + `storage/app/public` 이 존재 (Laravel 표준 구성)", + "복구 동작: 손상된 디렉토리를 `public/storage.broken.YYYYMMDDHHMMSS` 로 보존 후 symlink 재생성", + "운영자 조치: 자동 복구 후 `.broken` 디렉토리의 콘텐츠를 검증한 뒤 수동 삭제 (false positive 방어를 위해 자동 삭제하지 않음)" + ] + }, + { + "name": "Added", + "items": [ + "본인인증 관리자 권한을 조회/수정으로 분리 — \"프로바이더 설정 조회\" / \"정책 조회\" 권한 신설로 단순 조회 권한만 부여받은 운영자도 환경설정 본인인증 화면에 접근 가능 (기존에는 수정 권한이 없으면 조회 화면도 403)", + "환경설정의 본인인증 정책/메시지 탭에 권한 기반 버튼 비활성화 적용 — 수정 권한이 없는 운영자에게는 \"정책 추가\" / \"수정\" / \"삭제\" / \"활성 토글\" 버튼이 자동으로 비활성화 (이커머스/게시판 본인인증 탭 동일)", + "코어 업데이트의 spawn 자식 프로세스 실패 시 abort/fallback 동작 모드 선택 (`G7_UPDATE_SPAWN_FAILURE_MODE`, 기본 `abort`) — 부모 메모리 stale 로 인한 upgrade step fatal 위험을 fail-fast 로 차단 (#28 @bigmsg 님께서 제보해주셨습니다.)", + "업그레이드 스텝에 \"버전별 데이터 스냅샷\" 규약 도입 — 멀티 버전 점프 시에도 사용자가 단계별로 업그레이드한 것과 동등한 결과를 보장하도록 각 스텝의 시드 카탈로그·적용 로직·단발성 핫픽스를 해당 버전 디렉토리로 격리. 외부 확장 작성자가 beta.5+ 신규 업그레이드 스텝 작성 시 적용 (상세: 업그레이드 스텝 가이드) (#29 @bigmsg 님께서 건의해주셨습니다.)", + "언어팩 일괄 활성화 검증 규칙에 동적 확장 훅 추가 (`core.language_packs.bulk_activate_validation_rules`) — 모듈/플러그인이 호스트 확장 재활성화 cascade 흐름에서 추가 검증 필드를 동적으로 등록 가능" + ] + }, + { + "name": "Changed", + "items": [ + "본인인증 관리자 권한 키 정리 — 기존 `core.admin.identity.manage` / `core.admin.identity.policies.manage` 가 각각 `core.admin.identity.providers.update` / `core.admin.identity.policies.update` 로 변경. beta.4 환경에서 기존 권한을 부여받은 운영자 역할은 업그레이드 시 자동으로 새 권한 키로 마이그레이션 (부여 시점/부여자/스코프 메타데이터 모두 보존). 외부 확장 또는 운영 도구가 옛 키 문자열을 하드코딩한 경우 새 키로 갱신 필요" + ] + }, + { + "name": "Fixed", + "items": [ + "코어 업데이트 시 활성 모듈/플러그인/템플릿/언어팩 디렉토리가 삭제되던 문제 수정 — beta.4 → beta.5 업그레이드 시 번들 원본에서 활성 디렉토리를 자동 복원하고, 외부 설치 확장은 업데이트 로그로 재설치 안내", + "코어 업데이트의 단계 전환 신호를 비정상 입력으로부터 차단하도록 검증 강화", + "본인인증 정책 응답의 생성/수정 일시가 사용자 타임존이 아닌 UTC ISO 문자열로 노출되던 문제 수정", + "백업/롤백 시 `public/storage` 등 symlink 가 target 디렉토리 내용으로 추적 복사되어 symlink 가 일반 디렉토리로 변질되던 문제 수정 (Linux 환경 한정 — Windows 는 권한 한계로 일반 디렉토리 폴백 + 수동 회복 안내 유지)", + "root/super user 환경에서 Composer 검증·설치가 비대화형 컨텍스트에서 실패하던 문제 수정 (코어 업데이트, 확장 의존성 설치, 인스톨러 포함) (#31 @glitter-gim 님께서 제보해주셨습니다.)", + "본인인증 활동 로그의 \"액션\" 열에 `identity.verify` / `identity.verify_failed` 가 번역되지 않은 키 그대로 노출되던 문제 수정 — 코어 액션 라벨 매핑이 누락되어 있던 부분을 추가", + "본인인증 단계에서 운영자가 지정한 본인인증 수단이 무시되고 항상 기본 수단(메일)으로 발행되던 문제 수정 — 가입 정책에 설정한 본인인증 수단과 API 요청에서 명시한 수단이 실제 인증 발행에 반영되도록 변경" + ] + } + ] + }, + { + "version": "7.0.0-beta.4", + "date": "2026-05-11", + "categories": [ + { + "name": "Changed", + "items": [ + "레이아웃 `navigate` 액션의 기본 스크롤 동작이 페이지 이동 후 최상단 이동으로 변경됨 — 일반 하이퍼링크 이동 UX 와 일치. 이전처럼 스크롤 위치를 유지하려면 `scroll: \"preserve\"` 명시 필요. 검색 필터/페이지네이션 등 위치 유지가 필요한 화면에서 회귀가 의심되면 해당 옵션 추가 (engine-v1.45.0)", + "언어팩 매니페스트(`language-pack.json`) 를 모듈/플러그인/템플릿 매니페스트와 동일한 필드 구조로 정렬 — 외부 작성자가 다른 확장과 동일한 표준으로 언어팩을 만들 수 있도록 개선", + "언어팩 매니페스트에 GitHub 저장소 필드 추가로 GitHub 기반 업데이트 경로 지원 (모듈/플러그인 매니페스트와 동일한 동작)", + "관리자 환경설정의 언어팩 카드와 상세 모달이 모듈/플러그인/템플릿 카드와 동일한 형식으로 다국어 이름 · 설명 · GitHub 링크를 노출하도록 변경" + ] + }, + { + "name": "Added", + "items": [ + "관리자 환경설정 → SEO 탭에 Generator 메타 태그 카드 추가 — 토글로 노출 여부를 제어하고 내용 입력으로 W3Techs 등 CMS 시장 점유율 측정 도구가 인식하는 `` 태그를 SEO 봇 페이지·SPA·관리자 셸 모두에 출력. 내용 미입력 시 \"GnuBoard7 {버전}\" 자동 적용, 운영자가 버전 노출을 원치 않으면 \"GnuBoard7\" 만 입력 가능 (#26 @Lastorder-DC 님께서 건의해주셨습니다.)", + "인스톨러 4단계 확장 선택 화면에 \"언어팩\" 카드 신설 — 번들 언어팩을 locale 별 서브헤딩으로 노출하고, 사용자가 모듈/플러그인/템플릿을 선택하면 그 확장에 종속된 번들 언어팩 카드가 즉시 활성화. 종속 확장을 해제하면 그 언어팩 카드는 자동으로 비활성화 + 선택 해제", + "5단계 설치 진행 시 모든 확장의 install/activate 가 끝난 뒤 선택된 번들 언어팩을 일괄 설치 — 1건 실패는 best-effort 처리로 전체 설치를 중단하지 않음", + "코어/확장당 다수 locale(일본어 + 중국어 등)이 동시에 번들된 시나리오 대응 — 각 locale 은 독립된 서브헤딩 + 카드로 노출", + "인스톨러 시작 시 SSE 호환성을 사전 점검하여 환경에 맞는 모드(SSE 또는 폴링)로 단방향 진입 — 워커 동시 실행 race(테이블 / unique 키 중복 에러) 차단", + "SSE 비호환 환경 사용자에게 폴링 모드 진행 여부를 명시적 다이얼로그로 확인 후 시작", + "활동 로그의 액션 라벨이 모듈/플러그인 자체 다국어 파일에서 우선 해석되도록 변경 — 그동안 코어에 일괄 등록되어야 했던 모듈 origin 라벨(이커머스 주문·상품·마일리지, 게시판 게시물·댓글·신고, 페이지 등) 이 각 영역으로 이전되어 모듈/플러그인이 자기 도메인 라벨을 자기 영역에서 자기설명. 미정의 시 코어 라벨로 자동 fallback 하여 회귀 없음", + "모듈/플러그인이 발화하는 활동 로그가 호출자/대상 모델의 영역을 자동으로 인식하여 자기 다국어 파일로 라우팅 — 새 활동 로그를 추가할 때 별도 분기 코드 없이 자기 lang 만 채우면 정합", + "확장 entity 시더가 활성 언어팩의 다국어 데이터를 자동 머지하도록 다국어 시더 인터페이스 도입 — 신규 시더 작성 시 회귀를 자동 차단", + "번들 일본어 언어팩 빌드 스크립트가 시더 메타데이터를 일관된 경로로 조회하도록 단순화", + "다국어 라벨 표시 시 활성 언어팩의 lang key fallback 을 자동으로 처리하는 다국어 라벨 보강 helper 추가 — Provider/Registry 등록 페이로드(알림 채널·결제 PG 등) 와 settings JSON 다국어 데이터를 단일 시그니처로 처리", + "알림 채널(`config/notification.php` 의 `default_channels`) 의 라벨이 활성 언어팩(일본어 등) 으로 자동 보강되도록 변경 — 일본어 활성 시 한국어 fallback 으로 노출되던 회귀 차단", + "토스페이먼츠 PG 프로바이더 등록 페이로드가 lang key 기반으로 라벨을 선언하도록 변경 — 활성 언어팩으로 자동 보강", + "Provider/Registry 등록 페이로드가 다국어 JSON(`['ko' => ..., 'en' => ...]`) 을 직접 보유하지 않도록 audit 룰 신설 (회귀 자동 차단)", + "settings JSON 다국어 entry 의 식별 키(code/id/key) 보유 검증 audit 룰 신설 (lang pack fallback 키 조립 가능성 보장)", + "프론트엔드 `$localized()` 표현식이 두 번째 인수로 lang key fallback 을 받도록 확장 — 활성 로케일 라벨 부재 시 모듈/플러그인 언어팩의 키로 자동 보강", + "이커머스 환경설정 카탈로그(배송 가능 국가 / 통화 / 결제수단) 표시 시 활성 언어팩의 라벨이 자동 적용되도록 변경 — 일본어 활성 시 카탈로그 라벨이 한국어로 노출되던 회귀 차단 (다음 일본어 언어팩 빌드 시 자동 적용)", + "audit 룰이 `getDefault*Channels/Providers/Methods` 등 registry 기본값 반환 메서드의 다국어 JSON 직접 보유도 감지하도록 강화", + "환경설정 카탈로그(결제수단·통화·배송 가능 국가 등) 의 다국어 라벨이 카탈로그 빌드 시점에 활성 언어팩으로 자동 보강되도록 변경 — 사용자 체크아웃 + 관리자 환경설정 양쪽 모두 일본어 등 활성 시 한국어 fallback 으로 노출되던 회귀 차단", + "모듈/플러그인 개발자가 카탈로그를 추가할 때 누락하지 않도록 audit 룰이 helper 호출 누락 검출", + "[docs/backend/settings-multilingual-enrichment.md](docs/backend/settings-multilingual-enrichment.md) 에 단순 helper 사용 패턴 문서화", + "manifest `hidden: true` 플래그 추가 — 관리자 UI 목록에서 학습용/내부용 확장을 숨김 (CLI 는 정상 노출). 관리자 UI 에 슈퍼관리자 전용 \"숨김 포함\" 토글, artisan `module:list` / `plugin:list` / `template:list` 에 `--hidden` 플래그, 관리자 API `/api/admin/{modules,plugins,templates}` 에 `include_hidden` 쿼리 파라미터 지원", + "학습용 최소 샘플 확장 4종 번들 추가", + "모듈: `gnuboard7-hello_module` (Memo CRUD + 훅 발행 시연)", + "플러그인: `gnuboard7-hello_plugin` (Action/Filter 훅 구독 시연)", + "Admin 템플릿: `gnuboard7-hello_admin_template` (Basic 컴포넌트 최소 셋)", + "User 템플릿: `gnuboard7-hello_user_template` (홈 + Memo 리스트 연동)", + "모듈/플러그인/템플릿 정보 모달에 \"지원 언어\" 섹션 추가 — 코어/번들/사용자설치 출처 배지로 한눈에 확인", + "모듈/플러그인/템플릿 인스톨러에 의존 확장 + 동반 번들 언어팩 동반선택 UI 추가 — 미선택 의존성에 종속된 언어팩은 자동 비활성화", + "인스톨러 요구사항 검증 단계에 언어팩 디렉토리 쓰기 권한 점검 + 권한 부여 안내 추가", + "사용자 수동 비활성화와 코어 버전 호환성으로 인한 자동 비활성화를 DB 수준에서 구분 — 자동 비활성화된 확장만 재호환 감지/원클릭 복구 대상이 되도록 분리 (#18 @laelbe 님께서 제보해주셨습니다.)", + "코어 업그레이드 후 자동 비활성화 확장이 다시 호환되면 관리자 대시보드에 \"다시 활성화\" 알림 표시 + 원클릭 복구 버튼 제공 (자동 재활성화는 하지 않음 — 운영자가 명시적으로 복구)", + "모듈/플러그인/템플릿 목록 화면 상단에 자동 비활성화 확장 안내 배너 추가 (코어 업그레이드 가이드 링크 동반)", + "업데이트 모달에 코어 버전 호환성 안내 + \"위험을 이해하고 강제로 진행\" 체크박스 추가 — 운영자가 위험을 인지하면 비호환 확장도 강제 설치 가능", + "관리자 대시보드 \"시스템 알림\" 카드 노출 — 코어 호환성 자동 비활성화/재호환 알림이 분기 렌더되며 개별 dismiss 지원", + "코어 업데이트 마무리 단계에서 sudo 환경 결함(파일시스템 ACL · immutable 비트 · NFS 권한 거부 등) 으로 일부 경로의 소유권/그룹 쓰기 권한을 정상화하지 못한 경우 즉시 콘솔에 실패 경로 + 운영자 수동 복구 명령(`sudo chown -R …` / `sudo chmod -R g+w …`) 안내 — 이전에는 silent fail 로 묻혀 운영자가 후속 권한 거부 발생 후에야 인지하던 문제 해소", + "권한 기반 수신자 타입 추가 — `permission` 타입으로 특정 권한을 가진 모든 사용자에게 알림 발송 가능 (예: 게시판 신고 알림은 신고 관리 권한자에게 자동 발송)", + "모듈 환경설정에서 알림이 비활성화된 경우 발송 자체를 사전 차단하는 정책 게이트 도입 — 이전에는 수신자 해석 단계가 정책을 우회하여 발송되던 문제 해소", + "게시판 환경설정 → 신고 정책 탭에 신고 알림 채널 선택 UI 추가 (이메일 / 사이트 알림 다중 선택)", + "모듈/플러그인이 자기가 발송하는 알림 정의를 manifest 에서 직접 선언하도록 통일 — 활성화/업데이트 시 운영자 편집값을 보존하면서 자동 등록되고, 제거 시 함께 정리됨. 권한·메뉴·본인인증과 동일한 declarative getter 패턴", + "코어 기본 알림(회원가입 환영·비밀번호 재설정·비밀번호 변경) 의 다국어 제목/본문/변수/채널 정의를 `config/core.php` 로 통합 — 운영자가 코드 수정 없이도 향후 표면 변경을 추적할 수 있는 단일 소스 확보", + "본인인증(IdentityVerification) 인프라 도입 — 코어에 범용 IDV 프로바이더 계약을 마련하고, 기본 메일 프로바이더를 내장. 플러그인이 KCP·이니시스·SMS 등 다른 경로를 동일 계약으로 붙일 수 있도록 확장점 제공", + "외부 본인인증 provider (KCP·PortOne·토스인증·Stripe Identity 등) 가 G7 표준 Extension Point 패턴으로 자기 SDK UI 를 주입할 수 있는 슬롯 도입", + "비동기 검증 흐름을 위한 백엔드 폴링/콜백 엔드포인트 추가 — `GET /api/identity/challenges/{id}` (상태 폴링), `POST /api/identity/callback/{providerId}` (외부 redirect 콜백 수신)", + "본인인증 정책 시스템 신설 — 회원가입·비밀번호 재설정·민감 작업에 적용되는 모든 본인인증 시점을 라우트/훅 단위로 선언형 정책으로 통합 관리. 관리자 화면에서 정책 활성/유예 시간/프로바이더/실패 모드/단계(가입 제출 전 vs 가입 후 활성화 전)·적용 대상(self/admin/both)을 조정할 수 있으며, 운영자 수정값은 업데이트 재시딩 시 보존됨", + "본인인증 정책의 enable 토글이 라우트 코드 수정 없이 즉시 적용 — 모든 API 라우트가 정책 DB 와 자동 매칭되어 운영자가 admin UI 에서 정책을 켜는 즉시 본인인증이 강제됨. 코어/모듈/플러그인 어떤 라우트의 응답 처리 패턴에서도 본인인증 흐름이 일관되게 모달까지 도달하도록 처리", + "회원가입 단계 정책 2종 시드 — 가입 제출 전 동기 검증, 가입 후 활성화 전 비동기 challenge (기본값 비활성, 운영자 opt-in)", + "본인인증 정책이 활성화된 모든 강제 지점(회원가입·비밀번호 재설정·민감 작업·게시판/이커머스 정책 등)에서 사용자/관리자 화면에 동일한 모달 UX 가 자동으로 표시되도록 코어 인터셉터와 공통 모달 인프라 도입", + "전역 프론트엔드 인터셉트 — 서버가 HTTP 428 본인인증 요구 응답을 반환하면 자동으로 인증 모달을 띄우고 사용자가 인증에 성공하면 원 요청을 자동 재실행", + "본인인증 가드가 모듈/플러그인이 선언한 훅에도 자동 적용되도록 동적 구독 도입 — 결제 직전·민감 액션 직전 등 도메인 특화 가드를 운영자 토글 한 번으로 활성화", + "관리자 화면에 본인인증 정책 관리 페이지 추가 (정책 목록 DataGrid + 편집)", + "관리자 화면에 본인인증 이력 페이지 추가 — 알림 발송 이력과 동일한 수준의 UI 제공: 인증 수단(Provider) 탭(활성화된 프로바이더에 따라 자동 갱신), 통합 검색(자동 감지/사용자 ID/대상 식별자/IP/정책 키), 상태·인증 목적·채널·발생 유형 다중선택 OR 필터, 날짜 범위 + 고급 검색(발생 유형·출처·정책 키) 토글 영역, 정렬·페이지 사이즈 선택, 행 펼침으로 인라인 상세 표시, 모바일 반응형, 사용자 타임존 기준 시각, 보관주기(180일) 일괄 파기, 인증 수단/발생 위치 다국어 라벨 표시", + "본인인증 목적(purpose) 의 출처(source) 추적 — 코어/모듈/플러그인이 선언한 목적을 구분해 환경설정 화면에서 분리 표시하며, 코어 정책 화면 상단에 \"코어가 제공하는 본인인증 목적\" 칩 섹션(목적 코드/라벨 + 설명·허용 채널 툴팁) 추가", + "모듈 환경설정의 본인인증 정책 탭을 코어 정책 관리 화면과 동일한 UI/UX 로 통일 — 검색·필터(강제 시점/출처)·페이지네이션·정책 추가/편집 모달 직접 호출(코어 화면으로 이동 X)·이 모듈이 등록한 본인인증 목적 칩 섹션(없으면 안내 메시지). 정책 목록의 \"인증 목적\" 컬럼이 코드명 대신 사람이 읽을 수 있는 라벨로 표시", + "모듈/플러그인이 자기 컨텍스트의 본인인증 정책·목적·메시지 정의를 manifest 에서 직접 선언하도록 통일 — `module.php::getIdentityPolicies()` / `getIdentityPurposes()` / `getIdentityMessages()` 선언만으로 코어 정책 관리에 자동 연동되며, 활성화/업데이트 시 운영자 편집값을 보존하면서 자동 등록·정리됨. 권한·메뉴·알림과 동일한 declarative getter 패턴", + "본인인증 정책/이력 목록 API 가 source 컨텍스트별 필터를 받아 모듈 환경설정 탭이 자기 정책/이력만 조회 가능하며, 운영자 정의 정책을 특정 모듈/플러그인 컨텍스트에 귀속시켜 추가할 수 있도록 source_identifier 입력 허용 (`admin` / `module:{id}` / `plugin:{id}`)", + "본인인증 메일 메시지 템플릿 시스템 도입 — 프로바이더와 (목적/정책)별로 다국어 제목/본문을 개별 정의 가능하며, 메시지 발송 시 정책 → 목적 → 프로바이더 기본값 순서로 fallback 해석. 회원가입/계정 변경/중요 작업/비밀번호 재설정 + 프로바이더 기본값 등 5종 메일 템플릿이 한국어/영어로 시드되어 즉시 발송 가능", + "본인인증 challenge 발급/검증/취소 라우트에 권한 미들웨어 + scope=self 가드 적용 — 게스트는 `core.identity.{request,verify,cancel}` 권한으로 비로그인 가입(Mode B) 흐름 진입, 로그인 사용자는 본인 challenge 만 다룰 수 있으며 관리자는 임의 challenge 도 처리 가능. 모달 취소 시 서버 cancel API 를 호출해 challenge 가 audit log 에 cancelled 상태로 즉시 기록되도록 정합화", + "환경설정 → 본인인증 탭에 \"메시지 템플릿\" 서브탭 신설 — 알림 템플릿 관리와 동일한 UX (채널 서브탭·페이지당 항목 수 셀렉터·카드 펼침 본문 미리보기·활성/기본 배지·페이지네이션) 로 정의 목록·활성 토글·다국어 편집(변수 가이드 + 기본값 복원) 제공. 운영자가 추가한 정책에 전용 메일 메시지 정의를 화면에서 직접 등록·삭제 가능 (시드 기본값 보호 + 정책 키 매칭 검증). 외부 본인인증 프로바이더 플러그인이 자기 메시지 기본값을 코어 복원 로직에 기여할 수 있는 필터 훅 노출", + "알림 발송 이력 / 본인인증 이력 샘플 시더를 코어/모듈별로 분리 — 코어 시더는 코어 정의·정책만, 각 모듈 시더는 자기 영역만 채우도록 영역 격리. 모듈은 `module:seed {id} --sample` 으로 자기 영역 이력만 독립 생성 가능. 샘플 데이터는 실제 등록된 사용자·정의·정책·프로바이더 기반으로 생성되어 운영 데이터와 동일한 스키마/분포 유지", + "모듈/플러그인 제거 시 코어 공유 테이블에 적재된 해당 확장의 데이터(권한·관리자 메뉴·알림 정의·본인인증 정책·본인인증 메시지 정의·본인인증 목적) 가 함께 정리되며, 제거 모달의 \"삭제될 데이터\" 에도 항목별 건수가 표시되도록 개선", + "IDV 도메인 분류 데이터(목적·채널·트리거 출처·정책 범위·정책 실패 모드·정책 적용 대상·정책 출처·메시지 스코프) 8종을 PHP Backed Enum 으로 정의하여 정책 등록/수정 시 일관된 검증 적용", + "인스톨러 설치 완료 화면에 설치된 코어 버전 표시 — 사용자가 어떤 버전이 설치되었는지 즉시 확인할 수 있도록 개선", + "새 언어(일본어/중국어 등)를 코어 수정 없이 추가할 수 있는 언어팩(Language Pack) 시스템 도입", + "ZIP 업로드 또는 GitHub URL로 언어팩 설치/제거/활성화 지원", + "동일 언어 슬롯에 여러 벤더의 언어팩 공존 가능 — 라디오 전환으로 즉시 활성 변경", + "코어/모듈/플러그인/템플릿 별도 적용 — 모듈 언어팩은 해당 코어 언어팩이 활성일 때만 활성화 가능", + "관리자 메뉴: 환경설정 > 언어팩 관리(통합) + 모듈/플러그인/템플릿별 진입점 추가", + "사용자가 직접 수정한 다국어 키는 언어팩이 덮어쓰지 않도록 보존 정책 적용", + "보안: 언어 번역 외의 PHP 실행 코드 포함 시 설치 차단", + "언어팩 관리 화면을 모듈 관리와 동일 수준의 운영 도구로 보강 — 검색(식별자/벤더/언어), 업데이트 확인, 캐시 갱신, 다중 선택 일괄 제거 지원", + "활성/비활성 상태를 토글 스위치로 일원화 (보호된 팩은 비활성화된 토글로 표시)", + "업데이트 가능 항목에 \"업데이트 가능\" 배지와 행 단위 업데이트 실행 버튼 노출", + "정보 모달을 모듈 정보 모달과 동일한 4섹션 구조(기본정보 / 호환성 / 소스 정보 / 변경로그)로 제공하며, CHANGELOG.md 를 자동 파싱하여 버전별 카테고리로 표시", + "설치 모달에서 ZIP 업로드 전에 manifest 와 검증 결과를 사전 확인할 수 있는 미리보기 제공", + "모듈/플러그인/템플릿별 언어팩 페이지 진입 시 대상 확장 안내 배너와 환경설정 탭으로 회귀하는 링크 노출", + "언어팩 업데이트 권한을 별도 권한 키로 분리하여 설치/관리 권한과 독립적으로 부여 가능", + "언어팩 운영 풀 패리티 — 모듈/플러그인/템플릿 관리와 동일한 안전 장치/확장성 적용", + "공식 일본어(ja) 번들 언어팩 12종 추가 — 코어, 주요 모듈(전자상거래/게시판/페이지), 주요 플러그인(CKEditor5/마케팅/토스페이먼츠), 기본 템플릿(admin/user)을 일본어로 즉시 사용 가능", + "언어팩 관리 화면에서 번들 언어팩을 모듈/플러그인 관리와 동일하게 \"미설치\" 상태로 노출 — 행별 \"설치\" 버튼으로 즉시 설치 가능", + "언어팩 목록 필터에 \"미설치 (번들)\" 상태 옵션 추가", + "본인인증 메일 메시지 정의의 다국어 키를 언어팩으로 주입할 수 있도록 확장 — 코어 수정 없이 언어팩 ZIP 만으로 IDV 메일 본문/제목을 다국어화 가능", + "모듈/플러그인이 선언한 알림/본인인증 메시지 정의의 다국어 키도 언어팩으로 주입되도록 정합 — 이전에는 hook 발화 누락으로 코어 정의에만 적용되던 동작 정상화", + "업데이트 시 자동 백업 + 실패 시 직전 버전으로 자동 복구", + "동시성 가드 — 진행 중인 작업 동안 활성/비활성/제거/재업데이트 진입 차단", + "번들 디렉토리(`lang-packs/_bundled/{identifier}`) 에서 외부 다운로드 없이 (재)설치하는 경로 추가 — 코어 언어팩 복구/재배포 단순화", + "라이프사이클 훅 명명을 모듈/플러그인/템플릿과 통일 — `core.language_packs.{installed|updated|uninstalled|activated|deactivated}` 발행 (확장 가능성 확대)", + "Artisan 커맨드 신규 — `language-pack:list`, `language-pack:install`, `language-pack:update`, `language-pack:uninstall`", + "코어와 번들 확장(모듈/플러그인/템플릿)에 내장된 한국어/영어를 가상 보호 언어팩으로 자동 노출 — 별도 설치 없이 언어팩 관리자에서 항상 활성/보호 상태로 확인 가능, 수정/제거 차단", + "호스트 확장 비활성화 시 그에 종속된 언어팩이 함께 비활성화되며, 재활성화 시 \"다음 언어팩도 활성화하시겠습니까\" 모달로 사용자 의사 확인 후 일괄 활성화", + "여러 언어팩을 한 번에 활성화하는 `POST /api/admin/language-packs/bulk-activate` API 추가 (의존성/버전 호환성 자동 검사)", + "언어팩 활성화 시 의존성 + 호스트 확장 버전 호환성 검사 자동 수행 — 호스트 확장이 비활성/미설치이거나 버전 미달이면 활성화 차단", + "언어팩 업데이트 우선순위를 모듈/플러그인 패턴으로 정합화 — GitHub 1순위 + bundled 폴백, 강제 업데이트 시 bundled 우선", + "언어팩 상세 모달에서 코어/번들 확장 내장 항목 클릭 시 \"별도 언어팩이 아닌 내장 번역\" 안내 배너 노출", + "모듈/플러그인/템플릿 상세 모달의 닫기 버튼이 콘텐츠 길이와 무관하게 모달 하단에 항상 보이도록 sticky 처리", + "관리자 환경설정 > SEO 탭의 Sitemap 카드에서 sitemap 을 즉시 재생성하고 마지막 생성 시각을 확인할 수 있는 \"지금 생성\" 버튼 추가 — 큐 드라이버와 무관하게 동기 실행되며 결과를 즉시 표시", + "봇 감지 엔진을 `jaybizzle/crawler-detect` 라이브러리로 교체. 기본 약 1,000종의 봇(검색엔진·링크 미리보기·AI 검색 등)이 자동 감지됨 — 링크를 슬랙·페이스북·LinkedIn·트위터·디스코드·텔레그램 등에 붙여넣으면 제목·설명·이미지 미리보기가 즉시 동작", + "라이브러리가 놓치는 봇 3종(`kakaotalk-scrap`·`Meta-ExternalAgent`·`ChatGPT-User`) 을 G7 보강 패턴으로 기본 포함", + "봇 감지 확장 훅 `core.seo.resolve_is_bot` 신설 — 플러그인이 IP 범위 검증·역방향 DNS·Cloudflare 봇 점수 등을 주입할 수 있는 슬롯", + "\"봇 라이브러리 사용\" 관리자 토글 추가 (기본 on, 비활성 시 운영자 커스텀 목록만 사용하는 레거시 모드)", + "슬랙·페이스북 링크 미리보기에 이미지·카드가 표시되도록 OG 보강 태그를 자동 출력하도록 개선 — og:site_name, og:image:width, og:image:height, og:image:secure_url, og:image:type, og:image:alt, og:locale 추가", + "Twitter 카드 메타태그(twitter:card / twitter:site / twitter:title / twitter:image 등) 출력 신설 — 슬랙 unfurl 폴백 경로 정상화", + "운영자 환경설정 SEO 탭에 \"OG / Twitter 카드 기본값\" 카드 추가 — 사이트 이름·이미지 기본 가로/세로·Twitter 카드 타입·Twitter 사이트 핸들 5개 입력", + "모듈·플러그인이 자기 도메인의 OG/Twitter/JSON-LD 를 직접 선언하도록 SEO declaration API 신설 — 이커머스 상품(Product/Offer/AggregateRating)·게시판 게시글(Article) 등 도메인 스키마가 레이아웃에서 확장 코드로 owned 되어 데이터에 따라 정확한 부속 태그 생성", + "SEO 메타 확장 훅을 분기별·통합 모두 제공 — OG·Twitter·구조화 데이터 각각의 hook 슬롯과 통합 hook 모두 지원하여 확장이 원하는 단계에서 선택 변경 가능", + "모듈 설정 타이틀 템플릿에서 변수가 비어있을 때 인접 구분자(- – · |) 가 자동 정리되도록 개선 — 옵셔널 그룹 `[ ... ]` 표기도 지원하여 페이지별 구성 명시 가능" + ] + }, + { + "name": "Changed", + "items": [ + "콘솔 confirm 입력 처리 통일 — yes/y, no/n 외 입력 시 안내 메시지 출력 후 재질문, empty 입력 시 default 사용. 코어 업데이트·매니저 커맨드(module/plugin/template install·update·uninstall)·설정 마이그레이션의 모든 yes/no 프롬프트에 동일 규칙 적용 (#15 @laelbe 님께서 건의해주셨습니다.)", + "비밀번호 재설정 정책 기본값을 비활성으로 변경 — 본인인증 인프라가 미구성된 사이트에서도 기본 동작이 영향받지 않도록 운영자 opt-in 으로 전환", + "본인인증 정책의 인증 조건(`conditions`) 운영자 편집 허용 — 회원가입 단계 등 정책 조건을 코드 수정 없이 관리자 화면에서 조정 가능. 모듈 업데이트 시 운영자 수정값 보존", + "토큰 만료 등으로 권한 없는 레이아웃 진입 시 \"페이지 로딩 실패\" 에러 화면 대신 로그인 페이지로 자동 이동 — 로그인 화면에서 \"세션이 만료되었습니다. 다시 로그인해 주세요.\" 토스트로 사용자에게 안내. 템플릿이 자체 로그인 경로를 사용하는 경우 부트스트랩에서 인증 설정을 커스터마이즈할 수 있는 공개 API 도 함께 제공 (#19 @abc101 님께서 건의해주셨습니다.)", + "템플릿 다국어 데이터 로딩 시 활성 언어팩의 다국어가 가장 높은 우선순위로 병합되도록 변경", + "권한·역할·메뉴·알림 등 코어 기본 데이터와 배송유형·클레임 사유·게시판 유형 등 모듈 기본 데이터에 활성 언어팩의 다국어가 자동 반영되도록 개선", + "사용자 수정 보존(user_overrides) 정책을 다국어 JSON 컬럼은 sub-key 단위(`name.ko` 등) 로 기록하도록 개선 — 운영자가 한 언어 라벨만 수정해도 그 언어만 보존되며, 신규 활성 언어팩(예: 일본어 추가) 의 라벨은 자동 동기화됨. 기존 컬럼 단위(`name`) 기록은 업그레이드 시 활성 locale dot-path 로 자동 변환", + "언어팩 활성/비활성 시점에 영향받는 모듈/플러그인의 entity 시더가 자동 재실행되어 신규 언어 라벨이 즉시 DB 에 반영되도록 라이프사이클 통합 — scope 별 라우팅 (코어 언어팩 → 모든 활성 확장, 모듈 언어팩 → 해당 모듈만, 플러그인 언어팩 → 해당 플러그인만)", + "환경설정 → SEO → \"추가 봇 패턴\" 필드의 역할 변경 — 기존에는 유일한 봇 매칭 소스(기본 5종)였으나, 이제는 라이브러리가 놓치는 조직별 커스텀 봇만 추가하는 보강 레이어로 동작. 기존 설치의 운영자 커스텀 값은 모두 보존되며, 신규 설치 기본값은 jaybizzle 미커버 3종으로 변경" + ] + }, + { + "name": "Security", + "items": [ + "회원가입·비밀번호 재설정 라우트에 본인인증 정책 강제 미들웨어 부착 — 정책이 활성화된 경우 미인증 요청을 라우트 단계에서 차단 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)", + "플러그인이 정책 해석 필터 훅에서 잘못된 타입을 반환해도 원본 정책이 유지되도록 우회 차단 강화 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)", + "웹 인스톨러의 Composer/PHP 바이너리 경로 검증에서 사용자 입력이 그대로 shell 명령으로 실행될 수 있던 문제 수정 — 입력은 실행 가능한 단일 파일 경로로만 허용하고 모든 분기에서 인자 escape 강제. 설치 워커가 동일한 입력을 사용하던 내부 헬퍼도 같은 정책으로 정렬 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)", + "설치 단계 4 의 확장 기능 선택 API 가 사용자가 보낸 모듈/플러그인/템플릿/언어팩 식별자에 셸 메타문자 검증을 적용하도록 강화 — 부적절한 식별자는 400 응답으로 거부되어 이후 설치 명령에 도달하지 않음 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)", + "인스톨러의 코어 업데이트 _pending 경로 검증이 `..` 등 부모 디렉토리 우회 시도를 거부하고 응답 메시지를 단일화하여 임의 디렉토리 enumeration 신호 차단 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)", + "설치 시 `.env` 작성 헬퍼가 입력값에 포함된 개행 문자를 제거하도록 변경 — DB 비밀번호 등 사용자 입력으로 새로운 환경 변수 라인이 주입되는 시나리오 차단 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)", + "데이터베이스 연결 정보의 host/port/database 값에 DSN 키-밸류 구분자(`;`, `=`) 또는 NUL/CRLF 가 포함되면 연결을 거부하도록 추가 검증 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)", + "설치가 완료된 시스템에서 `public/install/` 하위 모든 엔드포인트가 비즈니스 로직 진입 전 HTTP 410 으로 차단되도록 공통 가드 도입 — 운영 환경에서 인스톨러 노출형 결함의 공격 표면 제거. 운영자가 인스톨러를 다시 사용해야 하는 경우 설치 완료 마커(`storage/app/g7_installed`) 와 `.env` 의 `INSTALLER_COMPLETED` 를 모두 제거 (KISA 측에서 제보해주셨습니다 — KVE-2026-0851)" + ] + }, + { + "name": "Fixed", + "items": [ + "코어 업데이트 후 일부 환경에서 모듈/플러그인이 저장한 사용자 데이터(상품 이미지·첨부파일 등)에 PHP 가 접근하지 못해 \"찾을 수 없음\" 오류가 발생하던 문제 수정 — 업데이트 종료 시점 권한 복원 범위를 PHP 쓰기 영역(storage/logs·storage/framework·bootstrap/cache·storage/app/core_pending)으로 한정하고 항목별 정확 복원으로 정합화. 이전 버전에서 본 릴리즈로 업그레이드한 환경에서는 업그레이드 시점에 storage/app 디렉토리/파일 권한을 PHP 가 접근 가능한 형태로 자동 정상화함. 향후 업데이트에서는 사용자 데이터 디렉토리에 자동 생성되는 보존 마커로 권한이 영구 보호됨", + "PHP 8.5 환경에서 모든 페이지 응답이 손상되어 Firefox 에서는 \"Content Encoding Error\", Edge/Chromium 에서는 빈 화면으로 표시되던 문제 수정", + "`php artisan serve` 환경에서 인스톨러 진행 중 .env 파일이 변경되면 개발 서버가 워커를 재시작하면서 설치 단계가 중도에 끊기던 문제 수정 — 설치 진행 중에는 .env 를 건드리지 않고 완료 화면 노출 후 한 번에 반영하도록 변경", + "관리자 환경설정 \"시스템 사양\" 카드의 메모리 항목이 서버 물리 메모리가 아닌 현재 PHP 프로세스가 사용 중인 메모리(수 MB 단위)로 표시되던 문제 수정 — 디스크 사용량과 동일한 형식(사용량/전체/백분율)으로 실제 서버 RAM 을 표시하도록 개선", + "관리자 환경설정 \"시스템 사양\" 카드의 CPU 항목이 Windows 11 / Windows Server 2025 에서 \"operable program or batch file.\" 로 표시되던 문제 수정 — 해당 OS 에서 제거된 wmic 의존을 걷어내고 PowerShell 기반 조회로 전환, 구형 Windows 환경에서는 기존 방식으로 자동 폴백", + "관리자 환경설정 \"정보\" 탭을 한 번 조회한 뒤 다른 탭으로 전환할 때마다 수 초 지연이 반복되던 문제 수정 — 시스템 정보 조회 결과를 1시간 캐싱하여 탭 전환 시 대기 시간 제거 (시스템 캐시 초기화 시 함께 무효화)", + "큐 워커가 훅 페이로드의 enum 값(주문 상태 등)을 복원하지 못해 활동 로그·알림 등 일부 후속 처리가 실패하던 문제 수정", + "항목 삭제 후 큐 워커가 처리하는 후속 훅에서 대상 데이터를 복원하지 못해 처리가 중단되던 문제 수정 — 소프트 삭제 페이지·첨부파일, 하드 삭제 주문 옵션 등 포함", + "페이지 키워드 검색 결과의 전체 건수가 발행된 모든 페이지 수로 부풀려지던 문제 수정", + "사용자가 댓글을 단 게시글 활동 조회 시 500 에러가 발생하던 문제 수정", + "사용자 활동 통계에 삭제된 게시글의 댓글 수가 포함되던 문제 수정", + "일부 주문에서 옵션 정보 직렬화 시 500 에러가 발생하던 문제 수정", + "상품 옵션 삭제 검증 시 런타임 에러가 발생하던 문제 수정", + "설치 직후 또는 활성 모듈 디렉토리 부재 시 이커머스 환경설정 기본값이 비어있던 문제 수정", + "검색 가능 드롭다운(SearchableDropdown)에서 빠른 모달 전환 시 race condition 가능성 차단", + "반응형 설정과 반복 렌더링이 결합된 레이아웃에서 무한 재귀가 발생할 수 있던 엔진 결함 수정 (engine-v1.43.1)", + "슬롯 치환 시 텍스트 노드를 컴포넌트로 가정하여 발생할 수 있던 레이아웃 렌더링 오류 차단", + "인스톨러 실행 시 보안 키 생성 단계에서 개발용 패키지 ServiceProvider 를 찾지 못해 설치가 중단되던 문제 수정 — 이전 환경에서 남은 컴파일 캐시를 vendor 교체 직후와 보안 키 생성 직전에 자동 정리하도록 개선", + "폴링 모드 인스톨러가 큰 확장(레이아웃·테스트 수천 파일) 설치 도중 멈추던 문제 수정 — 명령 출력을 파일 기반으로 처리하여 OS 파이프 버퍼 한도와 무관하게 동작", + "PHP 8.5 + Apache + mod_fcgid 환경에서 폴링 모드 진행 상황이 실시간 반영되지 않고 설치 완료 시점에 일괄 표시되던 문제 수정 — 어느 모드로도 진행 상황이 즉시 표시되도록 보정. Apache 환경별 권장 설정은 INSTALL.md 와 시스템 요구사항 문서에 명시", + "설치 완료 후 임시 파일이 자동 정리되지 않던 문제 수정", + "알림 템플릿 편집 모달의 입력 필드에서 글자를 입력할 때마다 화면이 심하게 버벅이던 문제 수정", + "슈퍼관리자가 다른 관리자 계정을 삭제할 수 없던 문제 수정 — 관리자 계정 삭제 가능 여부를 역할/권한/스코프 설정에 따라 판단하도록 개선 (슈퍼관리자 본인 보호는 유지)", + "`seo:generate-sitemap` 커맨드가 큐 드라이버 설정과 무관하게 항상 \"큐에 디스패치\" 안내를 출력하던 문제 수정 — 동기 드라이버에서는 즉시 생성으로 동작하고 그에 맞는 안내를 표시하도록 변경", + "관리자 템플릿을 활성화해도 즉시 반영되지 않아 사용자가 직접 새로고침해야 하던 문제 수정 — 관리자 템플릿 활성화 시 자동으로 페이지를 갱신하여 새 템플릿이 즉시 적용되도록 개선", + "보안 환경설정의 \"최대 로그인 시도 횟수 / 차단 시간\" 설정이 실제로 적용되지 않아 무제한 로그인 시도가 가능하던 문제 수정 — 임계 도달 시 계정 잠금(HTTP 423), 잠금 해제 시각 안내 토스트, per-IP 백업 throttle, 활동 로그 기록까지 통합 구현", + "게시판 글쓰기 화면을 URL 로 직접 진입하거나 강제 새로고침했을 때 업로드한 첨부파일이 게시글에 연결되지 않던 문제 수정 (engine-v1.49.2) (#24 @minyho 님께서 제보해주셨습니다.)", + "코어 업그레이드 후 새 버전에서 추가된 권한·메뉴·알림 정의가 등록되지 않아 관리자 화면에서 \"해당 권한이 없습니다\" 가 반복 표시되거나 신규 메일 템플릿이 비어있던 문제 수정 — 업그레이드 시 새 버전 설정 파일을 정확히 인식하도록 보정. 본 릴리즈로 업그레이드하면 누락분이 자동 등록됨", + "PHP 8.5 환경에서 관리자 로그인 시 `PDO::MYSQL_ATTR_SSL_CA` 등 PDO 드라이버 상수가 deprecation 경고를 발생시키던 문제 수정 — `Pdo\\Mysql::ATTR_SSL_CA` 형태의 신규 상수로 전환하고, PHP 8.5 미만 환경에서는 기존 상수를 그대로 사용하도록 분기 처리 (#23 @yks118 님께서 제보해주셨습니다.)", + "회원가입 시 일부 환경에서 동의 항목 메타데이터가 누락되어 활동 로그 처리에서 예외가 발생, 후속 훅 체인이 중단되며 신규 회원에게 `user` 역할이 자동 부여되지 않던 문제 수정 — 동의 메타데이터를 nullable 로 받아 누락 케이스에서도 후속 권한 부여가 정상 동작하도록 개선 (#25 @comtylove-netizen 님께서 제보해주셨습니다.)" + ] + } + ] + }, + { + "version": "7.0.0-beta.3", + "date": "2026-04-23", + "categories": [ + { + "name": "Fixed", + "items": [ + "CKEditor5 플러그인 활성 상태에서 게시판 글쓰기 저장 시 \"제목은 필수입니다\" 422 오류가 발생하던 호환성 문제 수정 — 제목·내용 입력 순서와 무관하게 정상 저장되도록 개선 (#17 @laelbe 님께서 제보해주셨습니다.)", + "코어 업데이트가 sudo 로 실행된 환경에서 캐시·세션·확장 디렉토리의 그룹 쓰기 권한이 일부 손실되어 업데이트 직후 \"Permission denied\" 또는 플러그인 제거 검증 실패가 발생하던 문제 수정 — 업데이트 종료 시점에 그룹 쓰기 권한을 자동 정상화하며 기존 손실 분은 1회성 복구 스텝으로 회수", + "업데이트 완료 후 Laravel 런타임이 새로 만드는 캐시·세션 하위 디렉토리가 기본 umask(022) 때문에 다시 그룹 쓰기 권한을 잃어 재차 \"Permission denied\" 가 발생하던 문제 수정 — 업그레이드 스텝이 업데이트 진행 프로세스의 umask 를 그룹 쓰기 친화적으로 전환하고, 이후 부팅 시점에도 `storage/` 의 현재 그룹 쓰기 설정을 감지해 프로세스 umask 를 자동 동조 (운영자가 그룹 공유를 비활성화한 환경은 그대로 보존)", + "코어 업데이트 마지막 단계의 진행 표시줄이 끝난 뒤 줄바꿈 없이 다음 셸 프롬프트가 같은 줄에 붙어 표시되던 출력 문제 수정" + ] + }, + { + "name": "Notes", + "items": [ + "7.0.0-beta.1 에서 7.0.0-beta.3 로 직접 업그레이드하는 경우, 환경(opcache CLI 활성 등)에 따라 권한 복구 스텝이 자동 실행되지 않고 건너뛰어질 수 있습니다. 업데이트 후 `storage/framework/cache` 등에서 Permission denied 가 발생하면 아래 명령을 수동 실행해 주세요 — 7.0.0-beta.2 에서 올라오는 경로에서는 해당 없음", + "`php artisan core:execute-upgrade-steps --from=7.0.0-beta.2 --to=7.0.0-beta.3 --force`", + "시스템 레벨에서도 일관된 그룹 공유 권한을 원하면 php-fpm pool 설정에 `umask = 002` (또는 systemd unit 의 `UMask=0002`) 추가를 권장합니다. 코드 레벨 동조와 병행하면 외부 프로세스(cron, composer 등) 도 동일 권한으로 파일을 생성합니다" + ] + } + ] + }, + { + "version": "7.0.0-beta.2", + "date": "2026-04-20", + "categories": [ + { + "name": "Added", + "items": [ + "알림 시스템을 Definition(정의) × Template(채널별 템플릿) × Recipients(수신자) 3계층으로 재설계", + "채널별 독립 수신자 설정 — trigger_user, related_user, role, specific_users 4종 타입 지원", + "채널 Readiness 검증 — 미설정 채널(SMTP 미구성 등)의 발송을 사전 차단", + "알림 발송 공통 디스패처 — 채널별 독립 발송, 발송 전후 훅 자동 실행, 모든 채널 자동 로깅", + "알림 클릭 URL 지원 — database 채널 알림에 클릭 시 이동할 URL 패턴 설정 가능", + "사용자/관리자 알림 전체 삭제 API 추가", + "비밀번호 변경 시 `password_changed` 알림 자동 발송", + "코어 권한에 사용자 알림 전용 카테고리 분리 (관리자/사용자 권한 의미 분리)", + "알림 정의 일괄 초기화 기능 — 커스터마이징된 알림의 모든 채널 템플릿을 기본값으로 복원 (확인 모달 + 스피너 UX 포함)", + "모듈/플러그인이 자신의 기본 알림 정의를 코어 리셋 로직에 제공할 수 있는 필터 훅 제공", + "템플릿 편집 시 편집 상태가 자동 추적되어 리셋 버튼이 자동 노출", + "알림 정의/템플릿 응답에 편집/삭제 권한 정보(abilities) 포함으로 UI가 권한에 따라 자동 조정", + "Composer 실행 불가 환경(공유 호스팅 등)을 위한 vendor 번들 시스템 추가", + "vendor-bundle Artisan 커맨드 — 빌드, 검증, 일괄 처리", + "모듈/플러그인 설치·업데이트에 `--vendor-mode=auto|composer|bundled` 옵션 추가", + "인스톨러 Step 3에 Vendor 설치 방식 선택 UI 추가 (환경 자동 감지)", + "MaxMind GeoLite2 자동 다운로드 스케줄러 + 관리자 환경설정 UI 추가", + "IANA 타임존 전체(약 425개) 지원 — 기존 7개 하드코딩 화이트리스트 폐기 (#8 @abc101 님께서 건의해주셨습니다.)", + "Select 컴포넌트에 `searchable` prop 추가 — 타임존 등 대량 옵션에서 검색 가능", + "SSE/폴링 듀얼 모드 지원 — Nginx 프록시 + Apache 환경에서 SSE 문제 시 폴링 모드로 전환 가능", + "확장 의존성 자동 해결 — 템플릿 선택 시 필요한 모듈/플러그인을 즉시 자동 선택, 전이적 의존성까지 해결 (#10 @glitter-gim 님께서 건의해주셨습니다.)", + "자동 선택된 항목을 시각적으로 구분하고 요구한 확장 이름을 함께 표시", + "다른 확장이 의존하는 항목은 선택 해제 차단 (의존 관계 안내 메시지 포함)", + "의존성 버전 제약 사전 검증 — 버전 불일치 시 설치 진행 전 경고 (semver 비교: `>=`, `^`, `~` 등 지원) (#3 @laelbe 님께서 제보해주셨습니다.)", + "기존 DB 테이블 감지 및 안전한 재설치 지원 — 백업 안내 + 명시적 동의 후 진행 (#5 @laelbe 님께서 건의해주셨습니다.)", + "권한 안내 단순화 — `chmod -R 755` 단일 명령어로 통합 (업계 표준 정렬)", + "소유자 불일치(`ownership_mismatch`) 감지 — 전통적 Apache 환경에서 3가지 해결 옵션 제시", + "Step 5에 \"설치 시작\" 버튼 도입 — 모드 선택 후 사용자 클릭으로 설치 시작", + "코어/모듈/플러그인 격리 캐시 시스템 도입 (접두사 자동 부여)", + "모델 변경 시 태그 기반 자동 캐시 무효화 트레이트 추가", + "환경설정에서 캐시 TTL 중앙 관리 (7개 설정 키)", + "페이지 진입/탭 전환 시 로딩 spinner 표시 — 데이터 fetch 완료까지 유지 (#6 @laelbe 님께서 제보해주셨습니다.)", + "목록 페이지네이션 시 DataGrid body 영역 한정 spinner (pagination 버튼 가림 방지)", + "DataGrid 컴포넌트에 `id` prop 추가", + "HookManager 공통 큐 디스패치 — 환경설정 하나로 모든 훅 리스너 자동 비동기 실행, 큐 워커에서도 Auth/Request/Locale 컨텍스트 자동 복원", + "WebSocket 클라이언트/서버 endpoint 분리 설정 지원 — 리버스 프록시 환경 대응, SSL 검증 옵션 추가", + "SEO 변수 시스템 — 모듈/플러그인이 페이지별 SEO 변수를 제공할 수 있도록 개선", + "코어 드라이버 확장 시스템 — 플러그인이 필터 훅으로 스토리지/캐시/세션/큐 등 드라이버를 등록 가능", + "`HasUserOverrides` 트레이트 — 시더 데이터와 사용자 수정을 자동으로 분리 추적", + "템플릿 엔진 engine-v1.42.0 — `render: false` 선택적 리렌더 제어", + "템플릿 엔진 engine-v1.41.0 — setLocal/dispatch debounce API + stale 값 오염 방지", + "템플릿 엔진 engine-v1.40.0 — navigate fallback 옵션 (교차 템플릿 경로 대응)", + "템플릿 엔진 engine-v1.30~38 — sortable wrapper 요소 지정, React.memo 자동 래핑, resolvedProps 참조 안정화, transition_overlay spinner 시스템, reloadExtensions 통합 핸들러", + "코어 업데이트 완료 후 `_bundled` 에 새 버전이 있는 확장을 감지하고 일괄 업데이트 여부를 묻는 인터랙티브 프롬프트 제공", + "일괄 업데이트 시 전역 레이아웃 전략(overwrite / keep) 1회 질의 후 확장별 예외 지정 가능", + "모듈/플러그인 업데이트에도 `--layout-strategy=overwrite|keep` CLI 옵션 및 관리자 UI 모달 선택 지원 (기존 템플릿 전용 → 확장 전체 일관)", + "관리자 모듈/플러그인 업데이트 모달에 사용자가 수정한 레이아웃 감지 및 목록 표시 기능 추가", + "모듈/플러그인 업데이트 시 각 upgrade step 버전을 콘솔에 실시간 출력 (기존에는 파일 로그에만 기록)", + "확장 업데이트 메서드 파라미터 순서를 3종(템플릿·모듈·플러그인) 공통 prefix(id, force, onProgress) 로 정렬해 일관성 확보", + "모듈/플러그인이 런타임에 동적으로 생성한 권한·역할·메뉴(예: 게시판별 권한 세트)도 업데이트 시 보존되도록 개선", + "모듈/플러그인/템플릿 업데이트 커맨드에 업데이트 소스 선택 옵션(`--source=auto|bundled|github`) 추가 — GitHub 원격 장애·태그 롤백 등 상황에서 번들 소스로 강제 설치 가능", + "GitHub 원격에 존재하지 않는 태그로 업데이트 시 \"아카이브 추출 실패\" 로 불명확한 오류가 나던 문제 개선 — 태그 존재 여부를 먼저 검증해 \"업데이트 소스 없음\" 안내로 명확화", + "관리자 모듈/플러그인 업데이트 모달의 _global 상태키를 `hasModifiedTemplateLayouts`, `hasModifiedModuleLayouts`, `hasModifiedPluginLayouts` 로 통일해 네이밍 충돌 방지", + "`module:install` / `plugin:install` / `template:install` 에 `--force` 옵션 추가 — 이미 설치된 확장도 `_bundled`/`_pending` 원본으로 활성 디렉토리를 덮어써서 재설치 (불완전 설치 복구용)", + "확장 활성 디렉토리 무결성 검사 추가 — `module.php`/`module.json` (모듈), `plugin.php`/`plugin.json` (플러그인), `template.json` (템플릿) 중 하나라도 누락된 활성 디렉토리를 로드 시 경고 로그 + `install --force` 복구 힌트 제공", + "코어 업데이트 커맨드에 외부 ZIP 파일 직접 지정 옵션(`--zip=/path/to/g7.zip`) 추가 — GitHub 다운로드 대신 지정 ZIP 을 사용해 오프라인/수동 배포 가능", + "모듈/플러그인/템플릿 업데이트 커맨드에 외부 ZIP 파일 직접 지정 옵션(`--zip=/path/to/ext.zip`) 추가 — manifest 식별자와 대상 확장이 일치하는지 검증 후 manifest 의 버전으로 업데이트", + "외부 ZIP 은 GitHub 릴리스 zipball 의 래퍼 디렉토리와 평탄 루트 구조를 모두 자동 감지해 지원" + ] + }, + { + "name": "Changed", + "items": [ + "MailTemplate 시스템을 NotificationDefinition + NotificationTemplate 체계로 전면 전환", + "코어 알림 3종의 직접 발송을 훅 기반 발송으로 전환", + "알림 수신자를 definition 레벨에서 template 레벨로 이동 (채널별 독립 수신자)", + "알림 broadcast 채널을 정수 User ID에서 UUID 기반으로 변경 (보안 강화)", + "알림 시각 표시를 ISO 8601에서 사용자 타임존 기준으로 변경", + "24개 서비스의 캐시 호출을 `Cache::` 파사드에서 `CacheInterface` DI로 전환", + "13개 서비스의 TTL을 환경설정 중앙 관리로 통일 (하드코딩 제거)", + "캐시 키 접두사를 `g7:core:`, `g7:module.{id}:`, `g7:plugin.{id}:` 체계로 통일", + "`CacheService` 클래스 삭제 — `CacheInterface`로 완전 대체", + "권한 검증을 비트 값 비교에서 실제 읽기/쓰기 가능 여부 기반으로 단순화", + "`.env` 생성 안내를 소유자 일치 여부에 따라 1-step으로 통합", + "확장 정보 모달에 의존성 정보 노출 — 코어 요구 버전(`g7_version`), 의존 모듈/플러그인 목록과 각각의 요구 버전·설치 버전·충족 상태 뱃지를 모듈/플러그인/템플릿 정보 모달에서 확인 가능", + "`locale_names` 설정 추가 — 언어 표시명을 중앙에서 관리", + "매 요청 `Schema::hasTable()` 호출 제거 — 설치 완료 환경에서 10~14회의 불필요한 스키마 쿼리 제거로 응답 시간 단축", + "SeoRenderer에 모듈/플러그인 SEO 변수 자동 해석 기능 추가", + "ResponseHelper — 디버그 모드에서 예외 상세 자동 포함, 프로덕션에서 내부 메시지 노출 차단", + "환경설정 드라이버 변경 시 큐 워커 자동 재시작", + "`allow_url_fopen=Off` 환경 지원 — GitHub 연동 HTTP 호출을 Laravel Http 파사드로 전면 교체", + "코어 업데이트 완료 후 안내 메시지를 \"수동 업데이트 명령 나열\"에서 \"일괄 업데이트 인터랙티브 프롬프트\"로 변경", + "확장 삭제 시 권한·메뉴·역할은 \"데이터도 함께 삭제\" 옵션을 체크했을 때만 삭제되도록 변경 — 기본 삭제에서는 보존되어 재설치 시 사용자 역할 할당이 복원" + ] + }, + { + "name": "Fixed", + "items": [ + "WebSocket 데이터소스가 progressive 목록에 포함되어 영구 블러되던 문제 수정", + "WebSocket 채널/이벤트 표현식 미평가로 인증 실패하던 문제 수정", + "WebSocket broadcast HTTP API가 클라이언트 host로 POST되어 실패하던 문제 수정", + "WebSocket 활성화 시 필수 필드 미검증 문제 수정", + "WebSocket 비활성화 시에도 Reverb 연결을 시도하던 문제 수정", + "`--force` 업데이트 시 번들 없는 외부 확장이 업데이트 소스를 찾지 못하던 문제 수정", + "확장 의존성 데이터 구조 불일치 수정 — manifest JSON 기반으로 통일", + "모듈/플러그인/템플릿 install/activate/deactivate/uninstall 직후 라우트 미반영 수정", + "다국어 번역 캐시 경합으로 raw key 노출되던 문제 수정 (engine-v1.38.1)", + "조건부 `$t:` 표현식에서 번역 실패하던 문제 수정 (engine-v1.38.2)", + "테스트 환경과 개발 환경이 동일한 Redis 캐시를 공유하여 상호 오염되던 문제 수정", + "대시보드 브로드캐스트 스케줄이 무한 락에 걸리던 문제 수정", + "환경설정 저장 후 `config:cache` 상태에서 변경값 미반영 수정", + "Windows 환경에서 파일 잠금 감지 시 30초+ hang 수정 — 0.5초로 단축", + "개발 의존성 없이 설치된 프로덕션 환경에서 샘플 시더와 팩토리가 실행되지 않던 문제 수정 — 한국어 샘플 데이터 생성기가 자동으로 대체 동작하도록 개선", + "`sudo php artisan core:update` 실행 시 vendor 디렉토리 전체가 root 소유로 오염되던 문제 수정", + "코어 업데이트 후 신규 권한·메뉴가 DB에 반영되지 않아 관리자 환경설정 페이지 접근이 거부되던 문제 수정", + "beta.1에서 업그레이드 시 제거된 메일 템플릿 모델 참조로 Fatal 오류가 발생하던 문제 수정", + "업그레이드 후 `bootstrap/cache` 등 일부 디렉토리가 root 소유로 남아 확장 설치·업데이트가 거부되던 문제 수정 — 소유권 복원 범위를 설치 안내 경로 전체로 확장", + "코어 업데이트 진행률 표시와 upgrade step 실행 로그가 같은 줄에 뒤섞이던 문제 수정 — 각 step 안내가 별도 줄로 깔끔하게 출력됨", + "템플릿/레이아웃 공개 API가 확장 활성화 전·설치 중에 받은 \"찾을 수 없음\" 에러 응답을 영구 캐시하여 복구 후에도 같은 오류를 반환하던 문제 수정 — 에러 응답은 캐시에서 제외", + "`_pending`/`_bundled` 에 업데이트 중 남은 임시 디렉토리(`{id}_YYYYMMDD_HHMMSS`, `{id}_updating_*` 등) 의 원본 manifest 때문에 `install` 이 존재하지 않는 표준 경로로 접근해 실패하던 문제 수정 — 디렉토리명과 identifier 불일치 시 스캔에서 제외", + "업그레이드 후 일부 코어 권한이 DB에 반영되지 않아 사용자 역할의 알림 기능 권한이 비어있던 문제 수정 — 별도 프로세스 실행으로 최신 로직 기반 재동기화 보장", + "코어·확장 설정에서 제거된 메뉴·권한·역할·알림 정의·알림 템플릿·게시판 유형·클레임 사유가 업데이트 후에도 DB에 잔존하던 문제 수정 — 고아 레코드 자동 정리", + "관리자 UI에서 메뉴·역할·알림·게시판 유형·클레임 사유·배송 유형 등을 수정해도 다음 업데이트 시 기본값으로 덮어써지던 문제 수정 — 사용자가 수정한 필드는 모든 저장 경로에서 자동 추적되어 업데이트 후에도 보존", + "알림 정의·템플릿도 업데이트 기준으로 동기화 — 새 버전에서 제거된 알림은 DB에서도 삭제", + "관리자 모듈/플러그인 목록의 \"작성자\" 표기가 manifest 의 `vendor` 필드 대신 식별자 prefix 를 사용하던 문제 수정 — `vendor` 가 정의된 확장은 정확한 작성자명으로 표시 (#9 @glitter-gim 님께서 제보해주셨습니다.)" + ] + }, + { + "name": "Removed", + "items": [ + "MailTemplate 시스템 일괄 제거 — 모델, 컨트롤러, 서비스, 리스너, 시더, 팩토리, 다국어 파일 등. NotificationDefinition + NotificationTemplate 체계로 완전 대체" + ] + } + ] + }, + { + "version": "7.0.0-beta.1", + "date": "2026-04-01", + "categories": [ + { + "name": "Changed", + "items": [ + "오픈 베타 릴리즈" + ] + } + ] + }, + { + "version": "7.0.0-alpha.21", + "date": "2026-03-30", + "categories": [ + { + "name": "Added", + "items": [ + "인스톨러 PHP CLI/Composer 필수 검증 기능 — 기본 `php` 미감지 시 CLI 설정 필수 전환, Composer 실행 확인 및 미설치 시 설치 안내, 둘 다 검증 완료 전 다음 단계 진행 차단", + "템플릿 레이아웃 수정 감지 시스템 — `original_content_hash`/`original_content_size` 컬럼 추가, SHA-256 해시 기반 수정 감지로 `updated_by` 방식 대체 (TemplateManager, LayoutRepository, 마이그레이션)", + "관리자 로그인 화면 개선 — 비밀번호 찾기/재설정 플로우 추가, 테마 Segmented Control, 언어 셀렉트 확대 (PasswordResetController, PasswordResetNotification, ForgotPasswordRequest, ResetPasswordRequest)", + "`{{raw:expression}}` 바인딩 번역 면제 마커 시스템 — `$t:` 자동 번역 대상에서 특정 바인딩을 제외하는 마커 도입 (rawMarkers.ts)", + "레이아웃 프리뷰 모드 — 관리자 레이아웃 편집기에서 실시간 미리보기 지원 (LayoutPreviewController, LayoutPreviewService, 마이그레이션)", + "활동 로그 이력 조회 메뉴 신설 — ActivityLogController, ActivityLogResource, ActivityLogService, ActivityLogRepository, config/core.php 메뉴/권한 등록, 관리자 레이아웃 추가" + ] + }, + { + "name": "Changed", + "items": [ + "`window.G7Config` 설정 노출 최소화 Phase 1 — 프론트엔드 미참조 설정의 브라우저 소스 노출 차단", + "SettingsService: 필드 레벨 `expose: false` 지원 추가 (formatCategorySettings)", + "FiltersFrontendSchema: `fields: {}` (빈 객체) 시 전체 차단 안전 기본값 적용", + "View Composers: TemplateComposer/UserTemplateComposer에 ModuleSettingsService 주입 — frontend_schema 기반 필터링 적용 (기존 config() 직접 참조 우회 수정)", + "코어 defaults.json: general 4개 필드, security 전체, seo 전체, advanced 9개 필드(debug_mode 제외), upload 2개 필드, drivers 전체를 `expose: false` 처리", + "캐시 무효화 로직 정비 — 016862cb 이전의 단순한 방식으로 복귀 (버전 증가 + TTL 자연 만료), `extension_cache_previous_versions` 추적 및 `getCacheVersionsToInvalidate()`/`getCacheVersionsForLayoutInvalidation()` 제거, Cache Tags 드라이버 분기 제거로 캐시 드라이버 비의존성 확보" + ] + }, + { + "name": "Fixed", + "items": [ + "MariaDB를 `DB_CONNECTION=mysql` 드라이버로 연결 시 `WITH PARSER ngram` 에러로 인스톨러 설치 실패 수정 — `isMariaDb()` 메서드 추가하여 서버 버전 문자열 기반 실제 DBMS 감지 (DatabaseFulltextEngine)", + "캐시 무효화 버전 키 누락 수정 — `warmTemplateCache()`가 버전 포함 키(`.v{version}`)로 캐시를 생성하나 `clearTemplateCache()`가 레거시 키만 삭제하던 문제, routes/language 캐시에 버전 포함 키 삭제 추가", + "캐시 버전 0 무효화 누락 수정 — `array_filter()`가 버전 `0`을 falsy로 제거하여 `.v0` 캐시 키가 삭제 대상에서 누락되던 버그 (ClearsTemplateCaches, InvalidatesLayoutCache)", + "테스트 활성 디렉토리 보호 강화 — `ProtectsExtensionDirectories`에 `moveDirectory` spy 추가, `copyToActive()` 원자적 교체가 활성 디렉토리를 rename으로 파괴하는 것 방지", + "활동 로그 하위 호환 — `ActivityLogResource`에서 기존 DB 레코드(`type: 'text'`)의 enum 필드를 모델의 현재 `$activityLogFields` 기준으로 동적 번역 + 일괄 변경 `:count` 미치환 보정", + "활동 로그 enum 미번역 전수 수정 — 14개 모델 필드의 `$activityLogFields` `type: 'enum'` 전환 + 14개 Enum 클래스에 `labelKey()` 메서드 추가 (User, Order, OrderOption, Product, Coupon, Post, Comment, Board)", + "활동 로그 삭제/일괄삭제 `description_key` 및 샘플 시더 누락 추가 — 코어 시더 `activity_log_descriptions` 테이블에 delete/bulk_delete 키 보충 + 다국어 파일 동기화", + "관리자 다크모드 품질 전수 조사 및 수정 — 레이아웃 JSON ~546건, TSX ~37건, CSS 5건의 누락된 `dark:` variant 추가 (이커머스 101파일, 게시판 19파일, 페이지 2파일, 마케팅 1파일, 템플릿 컴포넌트 ~20파일)", + "레이아웃 편집 캐시 무효화 및 버전 히스토리 수정 — LayoutService의 저장/복원 시 PublicLayoutController 서빙 캐시 무효화 + `extension_cache_version` 증가 누락 수정", + "사용자 관리에서 관리자/슈퍼관리자 삭제 시 토스트 메시지 및 어빌리티 처리 — CannotDeleteAdminException 추가, UserResource에 `can_delete` 어빌리티 반영", + "ActivityLog 핸들러 `$result` 타입 불일치 수정 — CoreActivityLogListener에서 `bool` → `array` 반환 타입 정합성 보정", + "환경설정 사이트 로고 저장 시 Attachment 객체 정수 검증 오류 수정 — SaveSettingsRequest에서 Attachment JSON 객체를 정수로 검증하던 문제 수정", + "관리자 SPA 네비게이션 로고 깜빡임 수정 — extends base 컴포넌트 불필요 remount 방지 (`_fromBase` 마킹 기반 stable key 패턴)", + "인스톨러 다크모드 적색 배경 가독성 개선 — `alert-title`, `alert-message`, `permission-badge`, `test-result` 다크모드 색상 override 추가", + "인스톨러 `BASE_PATH` 심볼릭 링크 미해석 — `realpath()` 적용 + 절대경로/상대경로 병기로 호스팅 환경 대응", + "인스톨러 403 에러 페이지 다크모드 미지원 — `prefers-color-scheme: dark` 미디어쿼리 추가", + "템플릿 업데이트 수정 감지 실패 — `LayoutRepository::update()`가 `updated_by`를 설정하지 않아 `hasModifiedLayouts()`가 항상 \"수정 없음\" 반환하던 문제 수정 (hash 비교 방식으로 전환)", + "템플릿 업데이트 \"수정 유지\" 전략 미작동 — `layoutStrategy === 'keep'` 시 `refreshTemplateLayouts()` 미호출 → 양쪽 전략 모두 호출하되 `preserveModified` 플래그로 분기", + "템플릿 업데이트 API 응답 키 불일치 — 백엔드 `has_modified` → 프론트엔드 기대 `has_modified_layouts`/`modified_count` 키 정렬", + "템플릿/모듈/플러그인 설치 시 `incrementExtensionCacheVersion()` 누락 수정 — TemplateManager(install/activate/deactivate/uninstall) + ModuleManager(install) + PluginManager(install) 총 7곳 추가", + "캐시 버전 변경 시 프론트엔드 다국어 미갱신 수정 — TemplateApp.ts에서 캐시 버전 변경 감지 시 TranslationEngine 재로드 추가", + "`warmTemplateCache()` 다국어 캐시 워밍 시 `$partial` 디렉티브 미해석 수정 — `json_decode`만 수행하던 코드를 `TemplateService::getLanguageDataWithModules()` 호출로 교체하여 fragment 해석 및 모듈/플러그인 다국어 병합 정상화", + "`ActivateTemplateCommand` 의존성 미충족 시 성공으로 보고하는 버그 수정 — `if ($result)` → `if ($result['success'])` 변경 및 의존성 경고 메시지 출력, `--force` 옵션 추가", + "`ActivateModuleCommand`/`ActivatePluginCommand` 의존성 미충족 시 경고 메시지 미출력 수정 — 의존성 경고 표시 및 `--force` 옵션 전달 추가" + ] + } + ] + }, + { + "version": "7.0.0-alpha.20", + "date": "2026-03-30", + "categories": [ + { + "name": "Added", + "items": [ + "Laravel Scout 통합 및 MySQL FULLTEXT(ngram) 검색엔진 드라이버 확장 시스템 도입 — 커스텀 `DatabaseFulltextEngine`으로 `MATCH...AGAINST IN BOOLEAN MODE` 지원, `core.search.engine_drivers` 필터 훅으로 Meilisearch/Elasticsearch 등 외부 엔진 플러그인 등록 가능", + "`DatabaseFulltextEngine` 다중 DBMS 호환 — FULLTEXT 미지원 DBMS(PostgreSQL, SQLite)에서 LIKE fallback 자동 전환, `whereFulltext()` 정적 헬퍼(관계 검색용), `addFulltextIndex()` 마이그레이션 헬퍼(DBMS별 조건부 DDL)", + "`config/scout.php` 설정 파일 추가 — 기본 드라이버 `mysql-fulltext`, `SCOUT_DRIVER` 환경변수로 전환 가능", + "`FulltextSearchable` 인터페이스 — FULLTEXT 검색 대상 컬럼 및 가중치 정의 계약", + "`AsUnicodeJson` 커스텀 캐스트 — JSON 컬럼에 한글을 `\\uXXXX` 이스케이프 없이 실제 UTF-8로 저장하여 FULLTEXT ngram 토크나이저 정상 동작 보장", + "환경설정 > 드라이버 탭에 검색엔진 설정 카드 추가 — 기본 MySQL FULLTEXT(ngram) 드라이버 표시, 플러그인 설치 시 추가 드라이버 자동 표시", + "`SaveSettingsRequest`에 검색엔진 드라이버 동적 validation 추가 — `core.search.engine_drivers` 필터 훅 기반 허용 목록" + ] + } + ] + }, + { + "version": "7.0.0-alpha.19", + "date": "2026-03-29", + "categories": [ + { + "name": "Added", + "items": [ + "검색/필터/정렬 성능 향상을 위한 누락 인덱스 일괄 추가 — activity_logs(description_key), users(created_at), mail_send_logs(status), template_layouts(template_id), schedules(created_at)" + ] + } + ] + }, + { + "version": "7.0.0-alpha.18", + "date": "2026-03-26", + "categories": [ + { + "name": "Added", + "items": [ + "SEO ExpressionEvaluator 산술 연산자 확장 — `*`, `/`, `%` 추가 (기존 `+`, `-`만 지원)", + "SEO PipeRegistry 파이프 함수 엔진 구현 — 프론트엔드 PipeRegistry.ts 빌트인 파이프 15종 PHP 미러링 (date, datetime, relativeTime, number, truncate, uppercase, lowercase, stripHtml, default, fallback, first, last, join, length, filterBy, keys, values, json, localized)", + "ActivityLog `description_params` ID→이름 변환 필터 훅 (`core.activity_log.filter_description_params`) — `ActivityLog::getLocalizedDescriptionAttribute()`에서 실행", + "코어 모델 `$activityLogFields` 정의 — `User`, `Role`, `Menu`, `Schedule`, `MailTemplate` (5개 모델)", + "ActivityLog ChangeDetector 필드 라벨 다국어 키 추가 (`lang/ko/activity_log.php`, `lang/en/activity_log.php` — `fields` 섹션)", + "`module_helpers.php` — `getModuleSetting()` 헬퍼 함수 추가", + "CoreActivityLogListener: bulk_update per-User/per-Schedule 전환 + bulk_delete per-Schedule 전환 (건별 loggable_id 기록)", + "ActivityLogHandler: 삭제된 엔티티용 loggable_type/loggable_id 직접 지정 fallback 지원", + "메뉴 관리 크로스 depth 이동 지원 — `UpdateMenuOrderRequest`에 `moved_items` 검증 추가, `MenuRepository`에 크로스 depth reorder 로직 구현", + "`NotCircularParent` 검증 규칙 추가 — 메뉴 순환 참조 방지" + ] + }, + { + "name": "Changed", + "items": [ + "ActivityLog 규정 문서 대폭 보강 (`docs/backend/activity-log.md`) — `description_params` 저장 정책, `ActivityLogDescriptionResolver` 패턴, Bulk Update ChangeDetector 패턴, 개발자 체크리스트 추가" + ] + } + ] + }, + { + "version": "7.0.0-alpha.17", + "date": "2026-03-26", + "categories": [ + { + "name": "Added", + "items": [ + "ActivityLog `description_params` ID→이름 변환 필터 훅 (`core.activity_log.filter_description_params`) — `ActivityLog::getLocalizedDescriptionAttribute()`에서 실행", + "코어 모델 `$activityLogFields` 정의 — `User`, `Role`, `Menu`, `Schedule`, `MailTemplate` (5개 모델)", + "ActivityLog ChangeDetector 필드 라벨 다국어 키 추가 (`lang/ko/activity_log.php`, `lang/en/activity_log.php` — `fields` 섹션)", + "`module_helpers.php` — `getModuleSetting()` 헬퍼 함수 추가", + "CoreActivityLogListener: bulk_update per-User/per-Schedule 전환 + bulk_delete per-Schedule 전환 (건별 loggable_id 기록)", + "ActivityLogHandler: 삭제된 엔티티용 loggable_type/loggable_id 직접 지정 fallback 지원" + ] + }, + { + "name": "Changed", + "items": [ + "ActivityLog 규정 문서 대폭 보강 (`docs/backend/activity-log.md`) — `description_params` 저장 정책, `ActivityLogDescriptionResolver` 패턴, Bulk Update ChangeDetector 패턴, 개발자 체크리스트 추가" + ] + } + ] + }, + { + "version": "7.0.0-alpha.17", + "date": "2026-03-26", + "categories": [ + { + "name": "Added", + "items": [ + "Monolog 기반 ActivityLog 아키텍처: `Log::channel('activity')` → `ActivityLogHandler` → DB (3단계)", + "`ActivityLogChannel` (커스텀 Monolog 채널), `ActivityLogHandler`, `ActivityLogProcessor` 신규", + "ActivityLog i18n 지원: `description_key` + `description_params` 기반 실시간 다국어 번역", + "ActivityLog 구조화된 변경 이력: `changes` JSON 컬럼 (필드별 `label_key`, `old`/`new`, `type` 포함)", + "`ChangeDetector` 유틸리티 (모델 스냅샷 비교 → 구조화된 변경 이력 생성)", + "`CoreActivityLogListener` 전면 확장: 모든 코어 Service 훅 구독 (User/Role/Menu/Settings/Schedule/Auth/Module/Plugin/Template/Layout/MailTemplate/Attachment — 66개 훅)", + "활동 로그 다국어 키 105개 정의 (`lang/ko/activity_log.php`, `lang/en/activity_log.php`)", + "`config/logging.php`에 `activity` 채널 추가", + "`config/activity_log.php` 전용 설정 파일 신규", + "`activity_logs` 테이블 복합 인덱스 추가 (`loggable_type`+`loggable_id`+`created_at`, `log_type`+`action`+`created_at`)" + ] + }, + { + "name": "Fixed", + "items": [ + "`resolveLogType()` 사용자 역할 기반 → 요청 경로 기반으로 변경: 관리자가 사용자 화면에서 수행한 액션이 `admin`으로 기록되던 문제 수정 (ResolvesActivityLogType)" + ] + }, + { + "name": "Changed", + "items": [ + "`ActivityLog` 모델: `description` 컬럼 삭제 → `description_key`/`description_params` 기반 다국어 전환", + "`ActivityLogService`: 기록 메서드 전면 제거 → 조회 전용으로 축소", + "`ActivityLogResource`: `description` → `localized_description` (실시간 번역)", + "모든 Controller에서 `logAdminActivity()` 호출 전면 제거 → Listener 경로로 전환" + ] + }, + { + "name": "Removed", + "items": [ + "`activity_logs.description` 컬럼 (DB 삭제)", + "`ActivityLogManager`, `ActivityLogDriverInterface`, `DatabaseActivityLogDriver`, `NullActivityLogDriver`", + "`ActivityLogListener` (이중 훅 계층 — Monolog Handler로 대체)", + "`ActivityLogService.log`/`logAdmin`/`logUser`/`logSystem` (Monolog 채널로 대체)", + "`AdminBaseController.logAdminActivity()`, `generateActivityDescription()`, `flattenDataForTranslation()`" + ] + }, + { + "name": "Fixed", + "items": [ + "마이페이지 프로필 저장 시 국가·언어 미선택 상태에서 오류가 발생하던 문제 수정 — 해당 필드를 선택 사항으로 변경", + "인스톨러 Step 2 .env 복사 명령어 안내 수정 — `.env.example.production` → `.env.example` (functions.php 2곳, installer.js 4곳)" + ] + } + ] + }, + { + "version": "7.0.0-alpha.16", + "date": "2026-03-23", + "categories": [ + { + "name": "Fixed", + "items": [ + "코어 업데이트 롤백 시 vendor 디렉토리 복원 불가 수정 — 백업 targets에 vendor 포함, excludes에서 vendor 제거", + "코어 백업 복원 시 개별 target 실패가 전체 복원을 중단하는 문제 수정 — 개별 try-catch로 나머지 target 복원 계속 진행", + "코어 업데이트 롤백 실패 시 수동 복구 안내 미출력 수정 — composer install 등 복구 단계 안내 추가", + "코어 업데이트 완전 복원 성공 시 유지보수 모드 자동 해제 추가", + "코어 업데이트 시 vendor 디렉토리 이중 처리로 인한 마이그레이션 실패 수정 — `backup_only` 설정 분리 (applyUpdate 제외, 백업/복원 전용)" + ] + } + ] + }, + { + "version": "7.0.0-alpha.15", + "date": "2026-03-23", + "categories": [ + { + "name": "Added", + "items": [ + "Users UUID 전환 — 외부 노출 ID를 UUID v7으로 전환, 정수 `id`는 API 응답에서 숨김", + "UniqueIdService 코어 서비스 추가 (UUID v7 + NanoID 생성)", + "코어 업그레이드 스텝 추가 (Upgrade_7_0_0_beta_15)" + ] + }, + { + "name": "Changed", + "items": [ + "User 모델: `getRouteKeyName()` → 'uuid', `$hidden`에 'id' 추가", + "공개 프로필 API: Route Model Binding 전환 (`{userId}` → `{user}`)", + "사용자 벌크 상태변경: 정수 ID → UUID 기반", + "API Resource: user.id → user.uuid 전환 (UserResource, UserCollection 외 7개)", + "Activity Log 메타데이터: user_id → uuid 전환", + "FormRequest: 정수 검증 → UUID 검증 전환" + ] + }, + { + "name": "Fixed", + "items": [ + "`_global.currentUser?.id` → `?.uuid` 전환 (sirsoft-basic 템플릿 12개 파일)", + "글쓰기 버튼 abilities 비활성화 누락 수정", + "코어 업그레이드 스텝 raw SQL 테이블 프리픽스 미적용 수정 — `UpgradeContext::table()` 헬퍼 추가 (Upgrade_7_0_0_beta_15)" + ] + } + ] + }, + { + "version": "7.0.0-alpha.14", + "date": "2026-03-20", + "categories": [ + { + "name": "Fixed", + "items": [ + "확장 업데이트 시 임시 디렉토리(`_updating_*`, `_old_*`) 오토로드 오염 방지 — 임시 디렉토리를 `_pending/` 하위에 생성하여 IDE 잠금 등으로 잔존 시에도 Fatal Error 방지 (ExtensionPendingHelper)" + ] + }, + { + "name": "Added", + "items": [ + "Windows 파일잠금 감지/해제 기능 — 확장 업데이트 시 IDE 등이 파일 핸들을 보유한 경우 자동 감지 및 해제 시도 (FileHandleHelper, ExtensionPendingHelper)", + "SEO 렌더러 훅 시스템: `core.seo.filter_context`, `core.seo.filter_meta`, `core.seo.filter_view_data` — 확장이 SEO 렌더링 파이프라인에 런타임 데이터 변환으로 개입 가능", + "`seo.blade.php` 확장 슬롯: `extraHeadTags` (`` 직전), `extraBodyEnd` (`` 직전) — `filter_view_data` 훅을 통해 커스텀 스크립트/스타일 주입", + "`ComponentHtmlMapper` pagination 렌더 모드 — Pagination 컴포넌트에서 SEO용 페이지 링크 자동 생성 (currentPage/totalPages props 기반)", + "`ComponentHtmlMapper` text_format dot notation 지원 — `{author.nickname}` 형태로 객체 prop의 중첩 필드 접근", + "`ExpressionEvaluator::evaluateRaw()` — 표현식 결과를 원본 타입(배열 등)으로 반환하는 메서드", + "`SeoRenderer` SEO 컨텍스트에 `_global`/`_local` 빈 객체 추가 — 프론트엔드 전용 상태 참조 시 null 대신 빈 객체 제공", + "`ComponentHtmlMapper` fields 렌더 모드 — 컴포지트 컴포넌트(ProductCard 등)의 객체 prop에서 SEO용 HTML 필드 자동 생성 (조건부/반복/속성 기반)", + "`SeoRenderer` seoVars 주입 — `meta.seo.vars` 선언을 해석하여 ComponentHtmlMapper format 모드에서 `{key}` 플레이스홀더 치환", + "`ExpressionEvaluator` 리터럴 값 감지 — 숫자, 문자열, boolean, null/undefined 리터럴을 경로 해석 없이 직접 반환", + "`ExpressionEvaluator` $t: 파라미터 `{{}}` 표현식 해석 — 번역 키 파라미터 값에 포함된 바인딩 표현식을 컨텍스트에서 평가", + "`TemplateManager` seo-config 검증에 `fields` 타입 추가", + "`ExpressionEvaluator` seo_overrides — seo-config.json에서 `_local`/`_global` 상태 오버라이드 선언 (와일드카드 매칭으로 접혀있는 콘텐츠 SEO 강제 펼침)", + "`TemplateManager` seo-config 검증에 `pagination` 타입 및 `seo_overrides` 검증 추가", + "`SeoConfigMerger` — 모듈/플러그인/템플릿의 seo-config.json을 수집·병합하는 동적 확장 시스템 (우선순위: 모듈 → 플러그인 → 템플릿, 24시간 TTL 캐싱)", + "`SeoRenderer` `_global` 컨텍스트 주입 — SettingsService/PluginSettingsService 프론트엔드 설정을 `_global`에 주입 + `initGlobal` 매핑으로 데이터소스 응답을 `_global` 경로에 바인딩", + "`AbstractModule`/`AbstractPlugin` SEO 기여 메서드 — `getSeoConfig()`, `getSeoDataSources()` 인터페이스 추가", + "`ModuleManager`/`PluginManager`/`TemplateManager` install/activate/update 시 훅 발행 추가 — Artisan 커맨드에서도 SEO 캐시 자동 무효화 보장", + "SEO Artisan 커맨드 다국어 파일 추가 (`lang/ko/seo.php`, `lang/en/seo.php`)", + "`ComponentHtmlMapper` fields 모드 `$all_props` source — 모든 props 표현식을 해석하여 데이터 객체로 사용 (Header/Footer 등 다수 props 컴포넌트용)", + "`ComponentHtmlMapper` fields 모드 `$t:` 번역 키 지원 — content 패턴에서 `$t:key` → 다국어 텍스트 렌더링", + "`ComponentHtmlMapper` fields iterate `item_attrs` — 아이템별 동적 HTML 속성 (예: `{ \"href\": \"/board/{slug}\" }`)", + "`ExpressionEvaluator` `evaluateRaw()` `??` null coalescing 지원 — 원본 타입(배열/객체) 유지하면서 null coalescing 수행", + "인스톨러 Windows 환경 명령어 대응 — `chmod`/`chown` 스킵, 미존재 디렉토리 안내 메시지 추가", + "`ExpressionEvaluator` 삼항 연산자 (`a ? b : c`) — JS 우선순위 준수, `?.`/`??` 자동 구분, 중첩 우측 결합", + "`ExpressionEvaluator` `$t()` 함수 호출 구문 — 삼항 내부에서 `$t('key')` 형태로 번역 키 사용 가능 (기존 `$t:key` 방식 확장)", + "`ExpressionEvaluator` `$localized()` 전역 함수 — 다국어 객체에서 현재 로케일 값 추출 (`{ko: \"상품\", en: \"Product\"}` → `\"상품\"`)", + "`ExpressionEvaluator` 객체 리터럴 파서 — `{key: value, ...obj, [dynamicKey]: value}` 구문 지원", + "`ExpressionEvaluator` 스프레드 연산자 — 배열 `[...arr, item]` 및 객체 `{...obj, key: value}` 스프레드 지원", + "`SeoRenderer` computed 속성 해석 — 레이아웃 `computed` 섹션을 `_computed`/`$computed`에 저장 (문자열 표현식 + `$switch` 형식)", + "`ComponentHtmlMapper` classMap 지원 — `base`/`variants`/`key`/`default`로 조건부 CSS 클래스 선언적 적용" + ] + }, + { + "name": "Fixed", + "items": [ + "Redis 캐시 DB가 환경설정 값(`REDIS_CACHE_DB`)을 따르도록 수정 — `config/database.php` cache 연결 DB 반영", + "`ExpressionEvaluator` 배열 리터럴 파싱 지원 — `['gallery','card'].includes(...)` 표현식에서 배열 리터럴을 PHP 배열로 변환하여 `includes` 등 배열 메서드 정상 동작 (SEO 게시판 타입 분기 조건 중복 렌더링 수정)", + "`ExpressionEvaluator` null 비교 JavaScript 시맨틱 적용 — `null !== value` → `true`, `null === null` → `true` (SEO 컨텍스트에서 `_global` 미존재 경로 비교 시 빈 문자열 대신 올바른 boolean 반환)", + "`ExpressionEvaluator` $t: 번역에서 `{{param}}` 형식 파라미터 치환 지원 — 템플릿 번역 파일의 `{{param}}` + Laravel 표준 `:param` 형식 모두 처리", + "`ExpressionEvaluator` 비교 연산 좌측 optional chaining 경로 타입 보존 — `?.` 포함 경로가 `evaluateExpression`으로 불필요 라우팅되어 boolean 타입이 문자열로 변환되던 문제 수정" + ] + } + ] + }, + { + "version": "7.0.0-alpha.13", + "date": "2026-03-18", + "categories": [ + { + "name": "Added", + "items": [ + "`SeoCacheRegenerator` — 단건 URL 캐시 즉시 재생성 서비스 (다국어 로케일별 렌더링 + 캐시 저장)", + "`SeoSettingsCacheListener` — 코어 SEO 설정 변경 시 전체 SEO 캐시 + 사이트맵 삭제" + ] + }, + { + "name": "Fixed", + "items": [ + "`SeoMiddleware`에서 `put()` 대신 `putWithLayout()` 사용 — `invalidateByLayout()`이 레이아웃명 미저장으로 항상 0건 매칭되던 근본 버그 수정", + "`SeoRenderer`에서 레이아웃명을 request attribute로 저장 — `putWithLayout()` 연동" + ] + } + ] + }, + { + "version": "7.0.0-alpha.12", + "date": "2026-03-17", + "categories": [ + { + "name": "Changed", + "items": [ + "API 인증을 토큰 전용으로 전환 — 세션 기반 인증 의존 제거, Bearer 토큰 단일 방식으로 통일", + "README 업데이트 — 표기 통일, 섹션 정비, 문서 링크 연결" + ] + } + ] + }, + { + "version": "7.0.0-alpha.11", + "date": "2026-03-16", + "categories": [ + { + "name": "Added", + "items": [ + "역할(Role) 상태 토글 API 추가 — `PATCH /api/admin/roles/{role}/toggle-status` 엔드포인트, 훅 지원 (`core.role.before_toggle_status`, `core.role.after_toggle_status`)", + "RoleResource에 `can_toggle_status` ability 추가 — 역할별 토글 권한 제어", + "RoleResource에 `extension_name` 필드 추가 — 확장 출처별 로케일 이름 표시", + "역할 생성 시 `identifier` 직접 입력 기능 추가 — 미입력 시 name 기반 자동 생성 유지", + "역할 관련 다국어 키 추가 (ko/en) — identifier 검증 메시지", + "`replaceUrl` 핸들러 추가 — refetch 없이 URL만 변경 (페이지네이션 등 브라우저 히스토리 관리용)", + "사용자 검색 API에 `id` 파라미터 지원 추가 — 특정 사용자 ID로 직접 조회 가능", + "`TimezoneHelper` 유틸리티 클래스 추가 — 사용자/서버 타임존 간 변환 헬퍼", + "`HasSampleSeeders` 트레이트 추가 — 모듈/플러그인 시더에서 `--sample` 옵션으로 샘플 데이터만 분리 실행", + "`ComponentRegistry.getComponentNames()` 메서드 추가 — 등록된 컴포넌트 이름 목록 조회" + ] + }, + { + "name": "Fixed", + "items": [ + "템플릿 에러 페이지에서 `:identifier`가 리터럴로 표시되는 버그 수정 — Blade `@` 이스케이프 처리", + "ActionDispatcher onChange raw value fallback 제거 — 마운트/리렌더 시 의도치 않은 setState 실행으로 상품 폼 회귀 유발", + "Form 자동 바인딩 setState 경합 수정 — 자동 바인딩과 수동 setState가 동시 실행 시 stale 값으로 덮어쓰이는 문제 해결", + "Form 자동 바인딩 bindingType 메타데이터 기반 boolean 바인딩 수정 — Toggle/Checkbox 등 boolean 컴포넌트에서 문자열 변환 대신 boolean 값 유지", + "SPA 네비게이션 시 `_global._local`에 이전 페이지 상태가 잔존하는 버그 수정 — 페이지 전환 시 `_local` 초기화 처리", + "DynamicRenderer `_computed` 참조 prop에서 캐시된 stale 값이 사용되는 버그 수정 — `_computed`/`$computed` 참조 시 `skipCache: true` 적용" + ] + }, + { + "name": "Changed", + "items": [ + "마이그레이션 통합 — 증분 마이그레이션을 테이블당 1개 create 마이그레이션으로 정리", + "시더 디렉토리 분리 — 설치 시더와 샘플 시더를 `Sample/` 하위로 분리, `--sample` 옵션 추가", + "라이선스 프로그램 명칭 정비", + "일정 관리 메뉴 기본 비활성화 (`config/core.php`)", + "Composer 의존성 업데이트 — Laravel Framework v12.54.1, Reverb v1.8.0, Symfony v7.4.6~7 등" + ] + } + ] + }, + { + "version": "7.0.0-alpha.9", + "date": "2026-03-13", + "categories": [ + { + "name": "Added", + "items": [ + "그누보드7 커스텀 에러 페이지 도입 — Laravel 기본 에러 페이지(401, 403, 404, 500, 503)를 그누보드7 스타일로 교체, 다크 모드 지원, 접근 경로 기반 홈 링크 분기 (admin → `/admin`, 기타 → `/`)", + "환경설정 > 고급 디버그 모드 활성화 시 개발 대시보드(`/dev`) 바로가기 버튼 추가", + "루트 LICENSE 파일 생성 (MIT 라이선스, 한국어 번역 + 영문 원문)", + "코어 라이선스/Changelog API 엔드포인트 추가 (`GET /api/admin/license`, `GET /api/admin/changelog`)", + "확장 라이선스 API 엔드포인트 추가 (`GET /api/admin/modules/{id}/license`, `GET /api/admin/plugins/{id}/license`, `GET /api/admin/templates/{id}/license`)", + "Admin 푸터 copyright 클릭 → 코어 라이선스 모달, 버전 클릭 → Changelog 모달 표시 기능", + "확장 상세 모달에서 라이선스 클릭 시 전문 모달 표시 기능 (모듈/플러그인/템플릿)", + "각 번들 확장에 LICENSE 파일 및 manifest `license` 필드 추가" + ] + }, + { + "name": "Changed", + "items": [ + "설치 화면 라이선스를 루트 LICENSE 파일로 통합 (`public/install/lang/license-ko.txt`, `license-en.txt` 삭제)", + "`/dev` 라우트 뷰 이름을 `dev-dashboard`로 변경" + ] + } + ] + }, + { + "version": "7.0.0-alpha.8", + "date": "2026-03-13", + "categories": [ + { + "name": "Changed", + "items": [ + "`.env.example.develop`과 `.env.example.production`을 `.env.example`로 통합 — 설치형 솔루션에 환경별 분리 불필요, Laravel/Vite 표준 준수", + "인스톨러에서 `.env.production` 백업 파일 생성 로직 제거 — Vite mode 기반 로딩 충돌 방지" + ] + }, + { + "name": "Improved", + "items": [ + "코어/확장 업데이트 시 composer install 스킵 최적화 — composer.json/composer.lock 미변경 시 composer install 및 vendor 디렉토리 교체를 건너뛰어 업데이트 시간 단축" + ] + }, + { + "name": "Fixed", + "items": [ + "확장 수동 설치 모달에서 설치 실패 시 상세 에러 사유(`errors.error`) 미표시 문제 수정 — 3개 모달에 상세 에러 P 요소 추가", + "`checkDependencies()` 복수 의존성 에러 수집 — 첫 번째 미충족 의존성에서 즉시 throw 대신 전체 수집 후 줄바꿈 연결 (ModuleManager, PluginManager, TemplateManager)", + "ModuleController/PluginController 에러 반환 형식을 TemplateController와 통일 — `['error' => $e->getMessage()]` 형태", + "확장 설치 실패 시 `_pending/{identifier}` 디렉토리 자동 정리 — ModuleService, PluginService, TemplateService에 try-catch 추가" + ] + } + ] + }, + { + "version": "7.0.0-alpha.7", + "date": "2026-03-13", + "categories": [ + { + "name": "Added", + "items": [ + "템플릿 엔진 `multipart/form-data` 지원 — apiCall 핸들러 및 DataSourceManager에서 `contentType: \"multipart/form-data\"` 설정 시 params를 FormData로 자동 변환", + "ActionDispatcher: `fetchWithOptions()`에 FormData 변환 로직 추가, Content-Type 헤더 자동 생략 (브라우저 boundary 설정)", + "DataSourceManager: `toFormData()` 메서드 추가, 인증/비인증 경로 모두 multipart 지원", + "File/Blob 원본 유지, null/undefined 제외, 객체/배열 JSON.stringify 변환", + "`deepMergeWithState()` non-plain 객체(File/Blob/Date) 보호 — spread 복사로 인한 내부 데이터 소실 방지", + "`resolveParams()` non-plain 객체 재귀 해석 스킵 — File 객체가 빈 객체로 변환되는 문제 방지", + "컴포넌트 onChange raw value fallback — FileInput, Toggle 등 Event가 아닌 값을 전달하는 컴포넌트 지원" + ] + }, + { + "name": "Fixed", + "items": [ + "DataSourceManager `isMultipart` 변수 TDZ(Temporal Dead Zone) 버그 수정 — 선언 전 참조로 인한 ReferenceError 해결" + ] + } + ] + }, + { + "version": "7.0.0-alpha.6", + "date": "2026-03-13", + "categories": [ + { + "name": "Fixed", + "items": [ + "플러그인 환경설정 페이지 진입 시 404 오류 수정 — `registerPluginLayouts()` admin/user 분기 도입 후 루트 `settings.json`이 스킵되던 문제", + "플러그인 설정 레이아웃을 `resources/layouts/settings.json` → `resources/layouts/admin/plugin_settings.json`으로 이동하여 모듈과 동일한 구조로 통일", + "`AbstractPlugin::getSettingsLayout()` 경로 변경", + "`PluginSettingsService` 오버라이드 경로 및 주석 수정", + "영향받는 플러그인: sirsoft-daum_postcode, sirsoft-marketing, sirsoft-tosspayments" + ] + }, + { + "name": "Changed", + "items": [ + "플러그인 설정 레이아웃 규정 문서(`plugin-development.md`) 경로/설명 업데이트" + ] + } + ] + }, + { + "version": "7.0.0-alpha.5", + "date": "2026-03-12", + "categories": [ + { + "name": "Added", + "items": [ + "확장(모듈/플러그인/템플릿) changelog GitHub 원격 소스 지원 — `source=github` 시 GitHub에서 CHANGELOG.md 조회, 실패 시 bundled 폴백", + "`ChangelogParser::parseFromString()`, `getVersionRangeFromString()` 문자열 기반 파싱 메서드 추가", + "`ChangelogRequest` validation 에러 다국어 메시지 추가 (source, version format, required_with)", + "플러그인 GitHub/ZIP 설치 기능 추가 — 모듈/템플릿에는 있지만 플러그인에 누락되어 있던 기능 신규 구현", + "`PluginService::installFromGithub()`, `installFromZipFile()`, `findPluginJson()` 메서드 추가", + "`PluginController::installFromFile()`, `installFromGithub()` 엔드포인트 추가", + "`InstallPluginFromGithubRequest`, `InstallPluginFromFileRequest` FormRequest 추가", + "`install-from-file`, `install-from-github` API 라우트 추가", + "`ExtensionManager::hasComposerDependenciesAt(string $path)` 메서드 추가 — 임의 경로의 Composer 의존성 확인", + "모듈/플러그인 설치 시 `_pending` Composer 선행 설치 로직 추가 — 활성 디렉토리 이관 전 의존성 설치" + ] + }, + { + "name": "Changed", + "items": [ + "확장 수동 설치 모달 3개(모듈/플러그인/템플릿) UI 통일 — TabNavigation underline, 에러 배너, 필드별 적색 테두리", + "확장 GitHub/ZIP 설치 공통 로직을 `GithubHelper`, `ZipInstallHelper`로 추출하여 3개 Service 중복 제거", + "`CoreUpdateService` GitHub 관련 protected 메서드를 `GithubHelper` 위임으로 리팩토링", + "확장 목록 PageHeader에서 새로고침 버튼 제거, 업데이트 확인 버튼으로 통일", + "코어 업데이트 `core_pending` 고정 경로 → `core_{Ymd_His}` 타임스탬프 기반 격리 디렉토리로 변경", + "확장(모듈/플러그인/템플릿) 업데이트에 `_pending/{identifier}_{timestamp}/` 스테이징 패턴 도입", + "확장 업데이트 시 스테이징 내에서 composer install 실행 (활성 디렉토리 무영향)", + "확장 GitHub 다운로드 코드를 코어와 동일한 패턴으로 통합 (인증 헤더, 폴백 체인, 타임아웃)", + "GitHub 다운로드 공용 로직을 `ExtensionManager`로 추출하여 3개 Manager 중복 제거", + "`config/app.php`에서 `preserves` 설정 키 제거 (타임스탬프 격리로 불필요)", + "모듈/플러그인 GitHub/ZIP 설치 흐름을 `temp → _pending → composer install → 활성 디렉토리` 패턴으로 통일" + ] + }, + { + "name": "Fixed", + "items": [ + "코어 업데이트 결과 모달에서 from/to 버전 파라미터가 전달되지 않는 버그 수정 — `params.params` → `params.query`", + "플러그인에 GitHub/ZIP 설치 기능이 누락되어 있던 결함 수정", + "파일 복사 시 퍼미션/소유자/소유그룹 미보존 문제 수정 — `File::copy()` → `FilePermissionHelper::copyFile()` 교체 (6개 위치)", + "Windows 환경에서 확장/코어 업데이트 후 `_pending` 하위에 빈 디렉토리가 잔존하는 문제 수정 — `cleanupStaging()`에 3단계 retry 로직 추가", + "코어 업데이트 후 vendor 교체 시 stale `packages.php`/`services.php`로 인한 500 오류 수정 — `clearAllCaches()`에 컴파일 캐시 삭제 + `package:discover` + `extension:update-autoload` 추가", + "코어 업데이트 시 `bootstrap/cache` 디렉토리가 소스에서 덮어씌워지는 문제 수정 — excludes에 `bootstrap/cache` 추가" + ] + } + ] + }, + { + "version": "7.0.0-alpha.4", + "date": "2026-03-12", + "categories": [ + { + "name": "Added", + "items": [ + "코어 업그레이드 스텝 검증용 샘플 마이그레이션 및 업그레이드 스텝 추가", + "코어 업그레이드 스텝 경로를 `database/upgrades/` → `upgrades/`로 변경", + "업그레이드 스텝 실행을 프로그레스바 별도 단계로 분리 및 터미널 피드백 추가" + ] + }, + { + "name": "Changed", + "items": [ + "`--source` 모드에서 원본 소스 디렉토리를 `_pending`으로 복제 후 작업 (원본 보호)", + "Step 8: 운영 디렉토리 `composer install` 재실행 → `_pending/vendor/` 복사로 변경 (효율화)" + ] + }, + { + "name": "Fixed", + "items": [ + "`--source` 모드에서 소스 버전 감지 시 현재 `env()` 대신 `config/app.php` default 값 파싱", + "코어 업데이트 targets에 `upgrades` 디렉토리 누락 수정" + ] + } + ] + }, + { + "version": "7.0.0-alpha.3", + "date": "2026-03-12", + "categories": [ + { + "name": "Fixed", + "items": [ + "`.gitattributes`의 `CHANGELOG.md export-ignore`가 모든 CHANGELOG 파일을 릴리스 아카이브에서 제외하던 문제 수정", + "PharData(tar ustar) 100바이트 경로 제한으로 324개 파일이 누락되어 orphan 삭제가 발생하던 버그 수정" + ] + }, + { + "name": "Removed", + "items": [ + "PharData 아카이브 추출 전략 제거 — tar ustar 형식의 100바이트 경로 제한은 근본적 해결 불가" + ] + }, + { + "name": "Added", + "items": [ + "`core:update --source=` 옵션 추가 — ZipArchive/unzip 불가 환경에서 수동 업데이트 지원", + "상대경로, 절대경로, Windows 경로 모두 지원", + "소스 디렉토리의 그누보드7 프로젝트 유효성 검증 (`config/app.php` + `version` 키)", + "업데이트 안내 모달에 수동 업데이트 가이드 섹션 추가", + "시스템 요구사항 미충족 시 `--source` 옵션 안내 메시지 추가" + ] + } + ] + }, + { + "version": "7.0.0-alpha.2", + "date": "2026-03-12", + "categories": [ + { + "name": "Fixed", + "items": [ + "코어 업데이트 확인 시 업데이트할 버전이 없으면 피드백 없던 문제 수정", + "`openModal` 호출 형식 수정 (`params.id` → `target`)", + "모달 데이터 바인딩 경로 수정 (`_local` → `$parent._local`)", + "코어 업데이트 명령어 에러 미출력 수정", + "`.env` 예제 파일에서 `G7_UPDATE_TARGETS` 하드코딩 제거" + ] + }, + { + "name": "Added", + "items": [ + "코어 업데이트 targets 확장 및 orphan 삭제 로직 추가", + "코어 업데이트 `--local` 옵션 및 설정 기반 제외/보존 추가", + "환경설정 고급 탭 코어 업데이트 설정 섹션 추가" + ] + } + ] + }, + { + "version": "7.0.0-alpha.1", + "date": "2026-03-07", + "categories": [ + { + "name": "Added", + "items": [ + "Laravel 12 기반 CMS 플랫폼 초기 구조 설계 및 구현", + "Service-Repository 패턴 기반 계층 분리 아키텍처 구축", + "CoreServiceProvider를 통한 인터페이스-구현체 바인딩 시스템", + "ResponseHelper를 통한 통일된 API 응답 형식 (success/error/paginated)", + "AdminBaseController / AuthBaseController / PublicBaseController 컨트롤러 계층 구조", + "FormRequest + Custom Rule 기반 검증 시스템", + "BaseApiResource 상속 기반 API 리소스 패턴", + "PHP 8.2+ Backed Enum 기반 상태/타입/분류 관리", + "모듈(Module) 시스템: 디렉토리 스캔 기반 자동 발견, 설치/활성화/비활성화/삭제 관리", + "플러그인(Plugin) 시스템: 모듈 의존 기반 기능 확장, 설정 UI(settings.json) 지원", + "템플릿(Template) 시스템: Admin/User 타입 분리, JSON 기반 레이아웃, 컴포넌트 레지스트리", + "ExtensionManager: 모듈/플러그인/템플릿 통합 관리 (설치, 업데이트, 삭제)", + "HookManager: Action/Filter 훅 시스템 (doAction, applyFilters, HookListenerInterface)", + "확장 업데이트 시스템: _bundled/_pending 디렉토리 구조, GitHub/로컬 업데이트 감지 및 적용", + "확장 백업/복원 시스템 (ExtensionBackupHelper)", + "확장 상태 가드 (ExtensionStatusGuard): Installing/Updating/Uninstalled 상태 관리", + "확장 권한 동기화 (ExtensionRoleSyncHelper): 설치/삭제 시 역할-권한 자동 동기화", + "확장 메뉴 동기화 (ExtensionMenuSyncHelper): 모듈 메뉴 자동 등록/해제", + "확장 오토로드 시스템: 런타임 Composer 오토로드 (composer.json 수정 불필요)", + "확장 Composer 의존성 관리 (extension:composer-install 커맨드)", + "확장 업그레이드 스텝 시스템 (UpgradeStepInterface, UpgradeContext)", + "확장 설정 시스템 (SettingsMigrator): 모듈/플러그인별 독립 설정 관리", + "확장 소유권 시스템: extension_type/extension_identifier 기반 리소스 귀속", + "확장 에셋 시스템: module.json 매니페스트 기반 JS/CSS 자동 로딩", + "확장 Changelog 시스템: ChangelogParser 헬퍼 + API 엔드포인트 + 관리 화면 인라인 표시", + "확장 빌드 시스템: Artisan 커맨드 기반 (module:build, template:build, plugin:build)", + "확장 캐시 관리: 확장별 독립 캐시 + 일괄 클리어 커맨드", + "DynamicRenderer: JSON 레이아웃 기반 React 컴포넌트 동적 렌더링", + "DataBindingEngine: `{{expression}}` 문법, Optional Chaining(`?.`), Nullish Coalescing(`??`) 지원", + "ActionDispatcher: 20+ 내장 핸들러 (navigate, apiCall, setState, openModal, closeModal, sequence, condition, replaceUrl, scrollTo, copyToClipboard, downloadFile, debounce, emit, showToast, confirm, validate, submit, reset, filter, sort 등)", + "ComponentRegistry: 컴포넌트 등록/검색/해석 시스템 (기본/집합/레이아웃 타입)", + "TranslationEngine: `$t:key` 즉시 평가 / `$t:defer:key` 지연 평가 다국어 바인딩", + "LayoutLoader: JSON 레이아웃 로딩, 캐싱, ETag 기반 조건부 요청", + "Router: SPA 라우팅, 동적 경로 파라미터(`{{route.id}}`), 쿼리스트링 관리", + "레이아웃 상속 시스템: `extends` 기반 베이스 상속 + `type: \"slot\"` 위치에 컨텐츠 삽입", + "Partial 시스템: 레이아웃 모듈화, 컴포넌트 치환 (data_sources/computed/modals/state 미지원)", + "조건부 렌더링: `if` 속성 기반 표현식 평가 (type: \"conditional\" 미지원)", + "반복 렌더링: `iteration` 설정 (source, item_var, index_var, key)", + "반응형 레이아웃: `responsive` 속성 기반 breakpoint 오버라이드 (portable/compact/wide)", + "다크 모드: Tailwind `dark:` variant 기반 자동 전환", + "classMap: 조건부 CSS 클래스 매핑 (key → variants)", + "computed: 계산된 속성 시스템 (의존성 추적, 자동 재계산)", + "모달 시스템: `modals` 섹션 + openModal/closeModal 핸들러 (`_global.modalStack` 기반)", + "init_actions: 레이아웃 초기화 시 자동 실행 액션 (루트 레이아웃 레벨)", + "Named Actions: 액션 재사용 시스템 (DRY 패턴)", + "errorHandling: 전역/데이터소스별 에러 핸들링 설정", + "scripts: 레이아웃 레벨 커스텀 스크립트 로딩", + "globalHeaders: API 호출 공통 헤더 설정 (pattern 기반 매칭)", + "blur_until_loaded: 데이터 로딩 전 블러 처리", + "lifecycle: 컴포넌트 생명주기 훅 (onMount, onUnmount)", + "slots: 컴포넌트 슬롯 시스템", + "layout_extensions: 모듈/플러그인의 동적 UI 주입 포인트", + "isolatedState: 컴포넌트 상태 격리", + "데이터소스 조건부 로딩: `if` 표현식 기반 활성화/비활성화", + "Basic 컴포넌트 (27+): Div, Button, Input, Select, Form, A, H1~H6, Span, P, Img, Label, Textarea, Table, Thead, Tbody, Tr, Th, Td, Ul, Ol, Li, Hr, Strong, Em, Small, Pre, Code, Blockquote, Nav, Header, Footer, Main, Section, Article, Aside, Figure, FigCaption", + "Composite 컴포넌트: DataGrid, CardGrid, Pagination, Modal, SearchBar, Tabs, TabPanel, Accordion, Badge, Breadcrumb, Card, Checkbox, CheckboxGroup, DatePicker, Dropdown, FileUpload, Icon, Notification, Radio, RadioGroup, RangeSlider, Rating, Select (enhanced), Sidebar, Stepper, Switch, Tag, Timeline, Toast, Tooltip, PasswordInput, DynamicFieldList, SortableList, ColorPicker, NumberInput, TreeView, DateRangePicker", + "Layout 컴포넌트: FlexLayout, GridLayout, ScrollLayout, StickyLayout, Spacer, Container, AspectRatio", + "HtmlEditor: TinyMCE 기반 HTML 에디터 컴포넌트", + "Icon 컴포넌트: Font Awesome 6.4.x Free 아이콘 지원 (Solid 1,390개 / Regular 163개 / Brands 472개)", + "Alert, EmptyState, LoadingSpinner, Skeleton, StatusBadge, CopyButton 유틸리티 컴포넌트", + "전역 상태 (`_global`): 앱 전체 공유, 페이지 이동 시 유지", + "로컬 상태 (`_local`): 레이아웃 단위 격리", + "계산된 상태 (`_computed`): 의존성 기반 자동 재계산", + "폼 자동 바인딩: Form 컴포넌트 `stateKey` 기반 자동 상태 연동", + "setState 핸들러: target(global/local), 함수형 업데이트, 배열 조작 (push/filter/map)", + "상태 구독: `G7Core.state.subscribe` 기반 반응형 업데이트", + "initGlobal: 전역 상태 초기값 선언", + "모달 스코프 상태: `$parent._local` 스냅샷 기반 데이터 전달", + "Laravel Sanctum 하이브리드 인증 (세션 + Bearer 토큰)", + "AuthManager: 싱글톤 인증 상태 관리", + "자동 토큰 갱신: 401 응답 시 자동 리프레시 후 재시도", + "OptionalSanctumMiddleware: 선택적 인증 지원 (비인증 사용자 허용)", + "로그인/로그아웃 3단계 프로토콜 (토큰 삭제 → 세션 무효화 → Auth::logout)", + "비밀번호 재설정: 이메일 인증 기반 토큰 발급 및 검증", + "회원가입: 약관 동의, 이메일 인증 지원", + "Role 기반 권한 관리 (User → Role → Permission 3계층)", + "permission 미들웨어 체인 기반 접근 제어 (FormRequest authorize() 사용 금지)", + "확장별 권한 자동 등록/해제", + "역할별 메뉴 접근 제어 (role_menus 피벗 테이블)", + "슈퍼 관리자 (superadmin) 전체 권한 자동 부여", + "계층형 메뉴 구조 (parent_id 기반)", + "역할별 메뉴 가시성 제어", + "모듈 메뉴 자동 등록 (getAdminMenus 인터페이스)", + "메뉴 순서 관리 (드래그 앤 드롭 SortableList)", + "다국어 메뉴명 지원 (JSON 배열 형식)", + "API 엔드포인트 선언적 정의 (id, endpoint, method, params)", + "loading_strategy: immediate/lazy/manual 3가지 로딩 전략", + "데이터소스 의존성: depends_on 기반 연쇄 로딩", + "폴링: poll_interval 기반 주기적 갱신", + "조건부 로딩: `if` 표현식 기반 활성화", + "transform: 응답 데이터 변환 함수", + "cache_duration: 응답 캐싱", + "대시보드: 시스템 정보, 통계 위젯, 최근 활동", + "사용자 관리: CRUD, 역할 할당, 상태 관리 (활성/비활성/차단)", + "역할 관리: CRUD, 권한 할당, 다국어 역할명", + "권한 관리: 카테고리별 권한 목록, 역할별 권한 할당", + "메뉴 관리: 계층형 메뉴 편집, 순서 변경, 역할별 가시성 설정", + "모듈 관리: 설치/활성화/비활성화/삭제, 업데이트 확인, 상세 정보 모달, changelog 표시", + "플러그인 관리: 설치/활성화/비활성화/삭제, 설정 UI, 업데이트 확인, changelog 표시", + "템플릿 관리: 설치/활성화/비활성화/삭제, 업데이트 확인, changelog 표시", + "환경설정: 사이트 기본 정보, SEO 설정, 메일 설정, 보안 설정, 탭 레이아웃", + "일정 관리: 스케줄 CRUD, 캘린더 뷰, 카테고리 분류", + "메일 템플릿 관리: DB 기반 메일 템플릿 CRUD, 변수 치환, 미리보기 기능", + "메일 발송 로그: 발송 이력 조회, 상태 추적, 상세 정보 모달", + "시스템 정보: PHP/Laravel/DB 버전, 디스크 사용량, 확장 현황 표시", + "코어 업데이트: 버전 확인, 업데이트 가이드, changelog 인라인 표시, 백업 생성", + "로그인/회원가입/비밀번호 재설정 페이지", + "마이페이지: 프로필 수정, 비밀번호 변경", + "통합 검색 기능", + "게시판 뷰: 목록/상세/작성/수정 (board 모듈 연동)", + "에러 페이지: 403, 404, 500, 503 커스텀 에러 페이지", + "점검 모드(maintenance) 페이지", + "다단계 웹 설치 마법사 (환경 체크 → DB 설정 → 관리자 생성 → 완료)", + "SSE(Server-Sent Events) 기반 실시간 설치 진행 상태 표시", + "설치 롤백 기능: 실패 시 자동 복원 (마이그레이션/시더 롤백)", + "언어 선택 지원 (한국어/영어)", + "다크 모드 지원", + "환경 요구사항 자동 검증 (PHP 버전, 확장, 디렉토리 권한)", + "게시판 관리: CRUD, 카테고리, 스킨 설정, 권한 설정", + "게시글 관리: CRUD, 검색, 정렬, 페이지네이션", + "댓글 시스템: CRUD, 대댓글 (계층형), 답글 알림", + "신고 시스템: 게시글/댓글 신고, 관리자 처리 (승인/거절)", + "첨부파일: 이미지/파일 업로드, 다운로드, 인라인 표시", + "비밀글: 작성자/관리자만 열람 가능", + "블라인드/복원: 관리자 블라인드 처리 및 복원 기능", + "카드/갤러리 레이아웃: 다양한 목록 표시 형태 지원", + "게시판 권한: 읽기/쓰기/댓글/관리 권한 분리", + "인기글/최신글 위젯", + "SEO 메타 태그 자동 생성", + "상품 관리: CRUD, 옵션(사이즈/색상), 라벨, SEO 메타, 이미지 갤러리", + "상품 카테고리: 계층형 카테고리, 순서 관리, TreeView 편집", + "브랜드 관리: CRUD, 로고, 설명", + "주문 관리: 주문 목록, 상태 변경, 상세 정보, 주문 타임라인", + "쿠폰 시스템: 정액/정률 할인, 사용 조건 (최소 금액, 특정 상품), 유효기간, 사용 횟수 제한", + "배송 정책: 무료/유료/조건부 배송, 지역별 요금 설정", + "공통 정보 관리: 배송/교환/환불 안내, 판매자 정보", + "장바구니: 추가/수량 변경/삭제, 옵션별 관리, 품절 상품 알림", + "체크아웃: 주소 입력 (다음 우편번호 연동), 배송지 저장 체크박스, 결제수단 선택, 쿠폰 적용", + "주문 완료: 주문 번호, 결제 정보 요약, 장바구니 자동 비우기", + "상품 상세 페이지: 이미지 갤러리, 옵션 선택, 수량 입력, 장바구니 담기", + "상품 목록: 필터링 (카테고리/브랜드/가격), 정렬, 페이지네이션, 카드 그리드", + "위시리스트: 찜하기/해제 기능", + "상품 검색: 키워드 검색, 카테고리 필터 연동", + "CMS 페이지: CRUD, 슬러그 기반 URL 매핑", + "페이지 버전 관리: 버전 이력 조회, 이전 버전 복원", + "첨부파일: 이미지/파일 업로드 지원", + "검색 통합: 페이지 내용 통합 검색 지원", + "SEO: 메타 태그, Open Graph 설정", + "다음 우편번호 검색 API 연동", + "주소 선택 후 폼 자동 입력", + "체크아웃 배송지 입력 연동", + "토스페이먼츠 결제 API 연동", + "카드/계좌이체/가상계좌/무통장입금 결제 지원", + "결제 확인 (confirm) 프로세스", + "결제 성공/실패 콜백 처리", + "관리자 설정 UI (API 키 관리)", + "마케팅 동의 관리: 이메일 구독, 마케팅 동의, 제3자 제공 동의", + "MarketingConsent / MarketingConsentHistory 모델 및 서비스", + "MarketingConsentListener 훅 리스너 (회원가입/프로필 연동)", + "사용자 관리 화면 레이아웃 확장 (마케팅 동의 상세/폼/프로필)", + "회원가입 폼 마케팅 동의 항목 확장", + "플러그인 설정 UI", + "역할 기반 접근 제어 (마케팅 관리자)", + "다국어 지원 (ko, en)", + "휴대폰 인증, 아이핀 인증 등 본인인증 기능 제공", + "사용자 관리 화면 레이아웃 확장 (본인인증 상세/폼)", + "역할 기반 접근 제어 (본인인증 관리자)", + "다국어 지원 (ko, en)", + "관리자 대시보드 레이아웃", + "사이드바 네비게이션: 접기/펼치기, 계층형 메뉴, 활성 상태 표시", + "상단바: 사용자 정보, 알림 벨, 다크 모드 전환 토글", + "반응형 레이아웃 (데스크톱/태블릿/모바일)", + "관리자 전용 컴포넌트: DataGrid, CardGrid, SearchBar, 필터 패널 등", + "CRUD 화면 표준 레이아웃 (목록/생성/수정/상세)", + "모달 기반 상세 보기/수정 기능", + "토스트 알림 시스템", + "확인 다이얼로그 (삭제 확인 등)", + "환경설정 탭 레이아웃", + "모듈/플러그인/템플릿 관리 화면 (설치/업데이트/상세 모달)", + "사용자/역할/권한 관리 화면", + "메뉴 관리 화면 (드래그 앤 드롭 순서 변경)", + "일정 관리 캘린더 화면", + "메일 템플릿 관리 화면", + "메일 발송 로그 화면", + "시스템 정보 화면", + "코어 업데이트 가이드/결과 모달", + "사용자 메인 페이지 레이아웃", + "헤더/푸터 공통 레이아웃 (반응형)", + "로그인/회원가입/비밀번호 재설정 화면", + "마이페이지 레이아웃", + "게시판 목록/상세/작성/수정 화면", + "상품 목록/상세 화면", + "장바구니/체크아웃/주문완료 화면", + "검색 결과 화면", + "CMS 페이지 표시 화면", + "에러 페이지 (403, 404, 500, 503)", + "점검 모드(maintenance) 전용 페이지", + "반응형 레이아웃 (데스크톱/태블릿/모바일)", + "백엔드 다국어: `__()` 함수 기반, `lang/{locale}/*.php` 파일 구조", + "프론트엔드 다국어: `$t:key` 즉시 평가 바인딩, `$t:defer:key` 지연 평가 바인딩", + "컴포넌트 다국어: `G7Core.t()` API 제공", + "모듈 다국어: 모듈별 독립 언어 파일, 네임스페이스 분리 (`__('vendor-module::key')`)", + "템플릿 다국어: partial 언어 파일 분할 (admin.json, errors.json 등)", + "DB 다국어 필드: JSON 배열 형식 (`{\"ko\": \"...\", \"en\": \"...\"}`)", + "지원 로케일: 한국어(ko), 영어(en)", + "TranslatableField 트레이트: 모델 다국어 필드 자동 해석", + "ValidLayoutStructure: JSON 레이아웃 구조 검증 Custom Rule", + "WhitelistedEndpoint: API 엔드포인트 화이트리스트 검증 Custom Rule", + "NoExternalUrls: 외부 URL 차단 검증 Custom Rule", + "ComponentExists: 컴포넌트 존재 여부 검증 Custom Rule", + "CSRF 보호: Laravel 기본 CSRF 토큰 검증", + "XSS 방지: 출력 이스케이프, HTML sanitize 처리", + "SQL Injection 방지: Eloquent ORM, 파라미터 바인딩 사용", + "Rate Limiting: API 요청 제한 미들웨어", + "DB 기반 메일 템플릿: 제목/본문 DB 관리, 변수 치환 (Blade 문법)", + "Notification + Mailable 통합 패턴: BaseNotification 상속으로 보일러플레이트 제거", + "메일 발송 로그: 발송 이력 DB 기록, 수신자/상태/발송 시간 추적", + "메일 템플릿 사용자 오버라이드 추적 (user_overrides 필드)", + "BaseNotification: 모든 알림의 베이스 클래스 (via() 보일러플레이트 제거)", + "알림 채널: 메일, 데이터베이스, 브로드캐스트 지원", + "실시간 알림: Laravel Reverb (WebSocket) 기반 Broadcasting", + "StorageInterface: 모든 파일 저장의 추상화 인터페이스 (Storage::disk() 직접 호출 금지)", + "CoreStorageDriver: 코어 스토리지 구현체", + "확장별 독립 스토리지 공간 할당", + "CoreUpdateService: GitHub API 기반 코어 버전 확인 및 업데이트 감지", + "CoreUpdateController: 업데이트 상태 확인, 가이드 표시 API 엔드포인트", + "CoreBackupHelper: 업데이트 전 코어 파일 백업 생성", + "FilePermissionHelper: 파일/디렉토리 쓰기 권한 사전 검증", + "MaintenanceModePage 미들웨어: 점검 모드 시 전용 페이지 표시", + "업데이트 가이드 모달: changelog 인라인 표시, 단계별 안내", + "MCP 서버: 20+ 디버깅 도구 제공", + "기본 도구: g7-state, g7-actions, g7-cache, g7-diagnose, g7-lifecycle, g7-network, g7-form, g7-expressions, g7-logs", + "고급 분석: g7-datasources, g7-handlers, g7-events, g7-performance, g7-conditionals, g7-websocket", + "상태 계층/스타일: g7-renders, g7-state-hierarchy, g7-context-flow, g7-styles, g7-auth, g7-tailwind, g7-layout", + "Phase 8 심화: g7-computed, g7-nested-context, g7-modal-state, g7-sequence, g7-stale-closure, g7-change-detection", + "브라우저 상태 덤프: 런타임 상태 캡처 및 MCP 서버 전송", + "UI 패널: 브라우저 내 DevTools 패널 (실시간 상태/액션/로그 확인)", + "페이지네이션: offset/limit 기반 대용량 데이터 조회 지원", + "비주얼 레이아웃 에디터: 드래그 앤 드롭 기반 레이아웃 편집", + "PropertyPanel: 컴포넌트 속성 편집 UI", + "컴포넌트 팔레트: 사용 가능한 컴포넌트 드래그 목록", + "실시간 미리보기: 편집 즉시 렌더링 결과 확인", + "레이아웃 캐싱: 파싱된 레이아웃 JSON + 상속 병합 결과 캐시", + "확장 캐싱: 모듈/플러그인/템플릿 목록 및 메타데이터 캐시", + "번역 캐싱: 언어 파일 파싱 결과 캐시", + "ETag 지원: API 응답 조건부 캐시 검증 (304 Not Modified)", + "Gzip 압축: API 응답 자동 압축", + "Debounce 액션: 연속 이벤트 디바운스 처리 핸들러", + "Lazy 로딩: 데이터소스 지연 로딩 (스크롤/이벤트 트리거)", + "순환 참조 방지 메커니즘: 레이아웃 상속 무한 루프 감지", + "코어 테이블: users, roles, permissions, role_has_permissions, role_menus, menus, settings, mail_templates, mail_send_logs, schedules, modules, plugins, templates, template_layouts, template_layout_versions", + "모든 컬럼 한국어 comment 필수 규칙 적용", + "down() 메서드 완전 롤백 구현 필수 규칙 적용", + "다국어 필드 JSON 배열 형식 표준", + "MariaDB 호환성 지원", + "boolean/enum 컬럼 값 설명 comment 포함 규칙", + "PHPUnit 11.x: 백엔드 단위 테스트 (Unit) 및 기능 테스트 (Feature)", + "Vitest: 프론트엔드 컴포넌트/템플릿 엔진 테스트", + "createLayoutTest(): 레이아웃 JSON 렌더링 테스트 유틸리티 (Vitest + jsdom)", + "mockApi(): API 응답 모킹 유틸리티 (fetch 자동 모킹)", + "트러블슈팅 회귀 테스트: 해결된 사례별 자동 검증", + "테스트 커버리지: 모델, 서비스, 컨트롤러, 컴포넌트, 레이아웃 렌더링", + "Vite 기반 프론트엔드 빌드 (코드 분할, 트리 쉐이킹)", + "Artisan 커맨드 기반 빌드 관리:", + "`core:build`: 코어 템플릿 엔진 빌드 (--full, --watch 옵션)", + "`module:build`: 모듈 프론트엔드 빌드 (--all, --watch, --active 옵션)", + "`template:build`: 템플릿 빌드 (--all, --watch, --active 옵션)", + "`plugin:build`: 플러그인 빌드 (--all, --watch, --active 옵션)", + "_bundled 디렉토리 기본 빌드, --active 옵션으로 활성 디렉토리 빌드", + "--watch 모드: 파일 감시 기반 실시간 빌드 (활성 디렉토리 자동 사용)", + "모듈 관리: module:list, module:install, module:activate, module:deactivate, module:uninstall, module:composer-install, module:cache-clear, module:seed, module:check-updates, module:update, module:build", + "플러그인 관리: plugin:list, plugin:install, plugin:activate, plugin:deactivate, plugin:uninstall, plugin:composer-install, plugin:cache-clear, plugin:seed, plugin:check-updates, plugin:update, plugin:build", + "템플릿 관리: template:list, template:install, template:activate, template:deactivate, template:uninstall, template:cache-clear, template:check-updates, template:update, template:build", + "확장 공통: extension:composer-install, extension:update-autoload", + "코어: core:build", + "AI 에이전트 개발 가이드 (AGENTS.md): 핵심 원칙, 코딩 규칙, 디버깅 프로토콜", + "규정 문서 체계 (docs/): 백엔드 14개, 프론트엔드 59개, 확장 23개, 공통 4개 (총 100개)", + "AI 에이전트 자동화 도구: 검증/분석/구현 스킬 30+개, 인덱스 자동 생성 스크립트", + "트러블슈팅 가이드: 상태 관리, 캐시, 컴포넌트, 백엔드 문제 해결 사례집", + "코드 스타일: Laravel Pint (PSR-12) 자동 적용" + ] + } + ] + } + ] + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -64,6 +1599,15 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/core-update/check HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) @@ -77,6 +1621,10 @@ _단건 응답: `data` 객체의 필드._ | latest_version | string | GitHub 릴리스에서 확인한 최신 버전 (조회 실패 시 현재 버전으로 대체) | | github_url | string | 버전 확인 대상 GitHub 저장소 URL (`config('app.update.github_url')`) | +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/dashboard.md b/docs/backend/api/dashboard.md index fd2b5d65..37fd1a3e 100644 --- a/docs/backend/api/dashboard.md +++ b/docs/backend/api/dashboard.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/dashboard/activities HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -39,6 +48,66 @@ _요청 파라미터 없음._ | time | string | `4시간 전` | 상대 시각 표시 (예: "24초 전" — diffForHumans() 산물) | | timestamp | string | `2026-07-07T10:00:47+09:00` | 활동 발생 절대 시각 (created_at 을 사용자 타임존으로 변환한 ISO 8601) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "최근 활동을 성공적으로 조회했습니다.", + "data": [ + { + "type": "admin", + "icon": "circle-info", + "icon_color": "blue", + "title": "사용자 목록 조회", + "description": "API 문서 샘플 사용자", + "time": "2분 전", + "timestamp": "2026-07-08T12:12:12+09:00" + }, + { + "type": "admin", + "icon": "circle-info", + "icon_color": "blue", + "title": "사용자 목록 조회", + "description": "API 문서 샘플 사용자", + "time": "6분 전", + "timestamp": "2026-07-08T12:08:26+09:00" + }, + { + "type": "admin", + "icon": "circle-info", + "icon_color": "blue", + "title": "사용자 목록 조회", + "description": "API 문서 샘플 사용자", + "time": "9분 전", + "timestamp": "2026-07-08T12:04:38+09:00" + }, + { + "type": "admin", + "icon": "circle-info", + "icon_color": "blue", + "title": "사용자 목록 조회", + "description": "API 문서 샘플 사용자", + "time": "43분 전", + "timestamp": "2026-07-08T11:31:03+09:00" + }, + { + "type": "admin", + "icon": "circle-info", + "icon_color": "blue", + "title": "사용자 목록 조회", + "description": "API 문서 샘플 사용자", + "time": "45분 전", + "timestamp": "2026-07-08T11:29:30+09:00" + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -61,12 +130,35 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/dashboard/alerts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "시스템 알림을 성공적으로 조회했습니다.", + "data": [] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -89,6 +181,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/dashboard/recent-notifications HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -102,6 +203,31 @@ _요청 파라미터 없음._ | time | string | `19시간 전` | 상대 시각 표시 (예: "24초 전" — diffForHumans() 산물) | | timestamp | string | `2026-07-06T18:20:23+09:00` | 발송 절대 시각 (sent_at, 없으면 created_at 을 사용자 타임존으로 변환한 ISO 8601) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "최근 알림을 성공적으로 조회했습니다.", + "data": [ + { + "id": 1, + "type": "apidoc.sample.event", + "channel": "mail", + "recipient": "API 문서 샘플 사용자", + "subject": "API 문서 샘플 알림", + "status": "sent", + "time": "2시간 전", + "timestamp": "2026-07-08T09:41:24+09:00" + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -124,6 +250,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/dashboard/resources HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -134,6 +269,37 @@ _단건 응답: `data` 객체의 필드._ | memory | object | `{"percentage":96,"used":"30.1 GB","total":"31.5 GB","colo…` | 메모리 사용량 정보 (percentage 사용률, used/total: 사용량·총량 형식화 문자열, color: 임계 색상). 수집 불가 시 percentage 0·"알 수 없음"·color gray 폴백 | | disk | object | `{"percentage":76,"used":"360.2 GB","total":"474.7 GB","co…` | 디스크 사용량 정보 (percentage 사용률, used/total: 사용량·총량 형식화 문자열, color: 임계 색상). 수집 불가 시 percentage 0·"알 수 없음"·color gray 폴백 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "시스템 리소스 정보를 성공적으로 조회했습니다.", + "data": { + "cpu": { + "percentage": 52, + "color": "blue" + }, + "memory": { + "percentage": 86, + "used": "27 GB", + "total": "31.5 GB", + "color": "yellow" + }, + "disk": { + "percentage": 76, + "used": "362.6 GB", + "total": "474.7 GB", + "color": "yellow" + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -156,6 +322,15 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/dashboard/stats HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -169,6 +344,48 @@ _단건 응답: `data` 객체의 필드._ | language_packs | object | `{"total":20,"active":16}` | 언어팩 집계 객체 (active: 현재 활성 언어팩 수, total: 활성 + 미설치 번들 팩 수) | | system_status | object | `{"status":"normal","label":"정상","all_services_running":true}` | 시스템 상태 객체 (status: normal 정상 / warning 경고, label: 상태 다국어 라벨, all_services_running: 전체 서비스 정상 동작 여부) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "대시보드 통계를 성공적으로 조회했습니다.", + "data": { + "total_users": { + "count": 2, + "change_percent": 0, + "change_display": "+2", + "trend": "up" + }, + "installed_modules": { + "total": 3, + "active": 3 + }, + "active_plugins": { + "total": 9, + "active": 0 + }, + "installed_templates": { + "total": 2, + "active": 0 + }, + "language_packs": { + "total": 17, + "active": 1 + }, + "system_status": { + "status": "normal", + "label": "정상", + "all_services_running": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/extensions.md b/docs/backend/api/extensions.md index 3b1cf1f5..8843fc97 100644 --- a/docs/backend/api/extensions.md +++ b/docs/backend/api/extensions.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/extensions/auto-deactivated HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -36,6 +45,27 @@ _단건 응답: `data` 객체의 필드._ | items | object | `{"plugins":[],"modules":[],"templates":[]}` | 코어 비호환으로 자동 비활성화된 확장을 타입별(`plugins`/`modules`/`templates`)로 묶은 목록. 각 원소는 식별자(`identifier`), 비호환 요구 버전(`incompatible_required_version`), 비활성화 시각(`deactivated_at`)을 가지며, 사용자가 dismiss했거나 hidden(학습용 샘플) 확장은 제외됨 | | current_core_version | string | `7.0.1` | 현재 설치된 코어 버전 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "자동 비활성화된 확장 목록입니다.", + "data": { + "items": { + "plugins": [], + "modules": [], + "templates": [] + }, + "current_core_version": "7.0.1" + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -61,10 +91,23 @@ _단건 응답: `data` 객체의 필드._ | type | path | string | 예 | module, plugin, template | 대상 확장의 타입 (module: 모듈, plugin: 플러그인, template: 템플릿). 타입에 맞는 Repository/Manager를 해석하는 데 사용되며 그 외 값은 422 | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +POST /api/admin/extensions/{type}/{identifier}/dismiss HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -91,10 +134,23 @@ _단건 응답: `data` 객체의 필드._ | type | path | string | 예 | module, plugin, template | 대상 확장의 타입 (module: 모듈, plugin: 플러그인, template: 템플릿). 타입에 맞는 Repository/Manager를 해석하는 데 사용되며 그 외 값은 422 | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +POST /api/admin/extensions/{type}/{identifier}/recover HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/identity.md b/docs/backend/api/identity.md index 4eefee74..adfc9fbd 100644 --- a/docs/backend/api/identity.md +++ b/docs/backend/api/identity.md @@ -49,6 +49,15 @@ | date_to | query | date | 아니오 | — | 조회 기간 종료일 | | per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +**요청 예시** + +```http +GET /api/admin/identity/logs?provider_id=%EC%98%88%EC%8B%9C%EA%B0%92&purpose=%EC%98%88%EC%8B%9C%EA%B0%92&status=%EC%98%88%EC%8B%9C%EA%B0%92&channel=%EC%98%88%EC%8B%9C%EA%B0%92&origin_type=%EC%98%88%EC%8B%9C%EA%B0%92&source_type=%EC%98%88%EC%8B%9C%EA%B0%92&source_identifier=example-key&provider_ids=%EC%98%88%EC%8B%9C%EA%B0%92&purposes=%EC%98%88%EC%8B%9C%EA%B0%92&statuses=%EC%98%88%EC%8B%9C%EA%B0%92&channels=%EC%98%88%EC%8B%9C%EA%B0%92&origin_types=%EC%98%88%EC%8B%9C%EA%B0%92&user_id=1&target_hash=%EC%98%88%EC%8B%9C%EA%B0%92&search=%EC%98%88%EC%8B%9C%EA%B0%92&search_type=auto&sort_by=created_at&sort_order=asc&date_from=2026-01-01&date_to=2026-01-01&per_page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -75,6 +84,34 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | verified_at | string | `2026-06-27 18:58:30` | verified 일시 | | expires_at | string | `2026-06-27 19:21:17` | expires 일시 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "data": [], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 0, + "from": null, + "to": null, + "has_more_pages": false + }, + "abilities": { + "can_purge": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -100,9 +137,45 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | older_than_days | body | integer | 아니오 | min 1, max 3650 | 파기 기준 보관일수 (지정 일수보다 오래된 이력만 삭제, 미지정 시 기본 180일) | +**요청 예시** + +```http +POST /api/admin/identity/logs/purge HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "older_than_days": 1 +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| purged_count | integer | `0` | purged 개수 (집계) | +| older_than_days | integer | `1` | 기준 경과 일수 (이 일수보다 오래된 대상 필터/집계) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "purged_count": 0, + "older_than_days": 1 + } +} +``` **에러 응답** @@ -141,6 +214,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity.message_definition.filter_index_rules`). +**요청 예시** + +```http +GET /api/admin/identity/messages/definitions?search=%EC%98%88%EC%8B%9C%EA%B0%92&provider_id=%EC%98%88%EC%8B%9C%EA%B0%92&scope_type=%EC%98%88%EC%8B%9C%EA%B0%92&scope_value=%EC%98%88%EC%8B%9C%EA%B0%92&extension_type=%EC%98%88%EC%8B%9C%EA%B0%92&extension_identifier=example-key&channel=%EC%98%88%EC%8B%9C%EA%B0%92&is_active=1&per_page=1&sort_by=%EC%98%88%EC%8B%9C%EA%B0%92&sort_order=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -166,6 +248,116 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | updated_at | string | `2026-06-30 13:33:16` | 최종 수정 일시 | | abilities | object | `{"can_update":true,"can_delete":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "IDV 메시지 정의 목록을 조회했습니다.", + "data": { + "data": [ + { + "number": 1, + "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 흐름)" + }, + { + "key": "expire_minutes", + "description": "만료까지 남은 분" + }, + { + "key": "purpose_label", + "description": "인증 목적 라벨" + }, + { + "key": "app_name", + "description": "사이트명" + }, + { + "key": "site_url", + "description": "사이트 URL" + }, + { + "key": "recipient_email", + "description": "수신자 이메일" + } + ], + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "is_active": true, + "is_default": true, + "user_overrides": null, + "templates": [ + { + "id": 1, + "definition_id": 1, + "channel": "mail", + "subject": { + "ko": "[{app_name}] 결제 본인 확인 인증 코드", + "en": "[{app_name}] Checkout Verification Code" + }, + "body": { + "ko": "

결제 본인 확인

결제를 진행하기 위해 본인 확인이 필요합니다. 아래 인증 코드를 입력해 주세요.

{code}

이 코드는 {expire_minutes}분 후 만료됩니다.

본인이 결제를 진행하지 않았다면 이 메일을 무시하고 즉시 비밀번호를 변경해 주세요.

감사합니다,
{app_name}

", + "en": "

Checkout Verification

Please enter the code below to proceed with payment.

{code}

This code will expire in {expire_minutes} minutes.

If you did not initiate this payment, please ignore this email and change your password immediately.

Thank you,
{app_name}

" + }, + "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 10:43:32", + "abilities": { + "can_update": true, + "can_delete": false + } + } + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + }, + "abilities": { + "can_create": true, + "can_update": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -200,10 +392,45 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity.message_definition.filter_store_rules`). +**요청 예시** + +```http +POST /api/admin/identity/messages/definitions HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "provider_id": "예시값", + "scope_type": "예시값", + "scope_value": "예시값", + "name": [ + "예시 이름" + ], + "description": [ + "예시 내용입니다." + ], + "channels": [ + "예시값" + ], + "variables": [ + "예시값" + ], + "templates": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -229,10 +456,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | definition | path | string | 예 | — | 대상 definition의 식별자 | +**요청 예시** + +```http +DELETE /api/admin/identity/messages/definitions/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -258,6 +498,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | definition | path | string | 예 | — | 대상 definition의 식별자 | +**요청 예시** + +```http +GET /api/admin/identity/messages/definitions/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -282,6 +531,98 @@ _단건 응답: `data` 객체의 필드._ | updated_at | string | `2026-06-30 13:33:16` | 최종 수정 일시 | | abilities | object | `{"can_update":true,"can_delete":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```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 흐름)" + }, + { + "key": "expire_minutes", + "description": "만료까지 남은 분" + }, + { + "key": "purpose_label", + "description": "인증 목적 라벨" + }, + { + "key": "app_name", + "description": "사이트명" + }, + { + "key": "site_url", + "description": "사이트 URL" + }, + { + "key": "recipient_email", + "description": "수신자 이메일" + } + ], + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "is_active": true, + "is_default": true, + "user_overrides": null, + "templates": [ + { + "id": 1, + "definition_id": 1, + "channel": "mail", + "subject": { + "ko": "[{app_name}] 결제 본인 확인 인증 코드", + "en": "[{app_name}] Checkout Verification Code" + }, + "body": { + "ko": "

결제 본인 확인

결제를 진행하기 위해 본인 확인이 필요합니다. 아래 인증 코드를 입력해 주세요.

{code}

이 코드는 {expire_minutes}분 후 만료됩니다.

본인이 결제를 진행하지 않았다면 이 메일을 무시하고 즉시 비밀번호를 변경해 주세요.

감사합니다,
{app_name}

", + "en": "

Checkout Verification

Please enter the code below to proceed with payment.

{code}

This code will expire in {expire_minutes} minutes.

If you did not initiate this payment, please ignore this email and change your password immediately.

Thank you,
{app_name}

" + }, + "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 10:43:32", + "abilities": { + "can_update": true, + "can_delete": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -313,10 +654,37 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity.message_definition.filter_update_rules`). +**요청 예시** + +```http +PATCH /api/admin/identity/messages/definitions/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "description": [ + "예시 내용입니다." + ], + "channels": [ + "예시값" + ], + "is_active": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -343,9 +711,130 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | definition | path | string | 예 | — | 대상 definition의 식별자 | +**요청 예시** + +```http +POST /api/admin/identity/messages/definitions/1/reset HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| provider_id | string | `g7:core.mail` | provider 식별자 (연관 리소스 참조) | +| scope_type | string | `purpose` | 메시지 정의 스코프 (provider_default\|purpose\|policy) — App\Enums\IdentityMessageScopeType enum | +| scope_value | string | `checkout_verification` | 범위 값: provider_default 빈 문자열 / purpose 키 / policy 키 | +| name | object | `{"ko":"결제 시 본인 확인","en":"Checkout Verification"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| description | object | `{"ko":"결제 진행 전 본인\/성인 확인 인증 코드 메일","en":"Identity\/adult …` | 설명 (다국어 필드는 로케일별 값 객체) | +| channels | array | `["mail"]` | 활성 채널 (현재 ["mail"], 향후 sms 등 확장) | +| variables | array | `[{"key":"code","description":"인증 코드 (text_code 흐름)"},{"ke…` | 사용 가능 변수 메타데이터 ([{key, description}]) | +| extension_type | string | `module` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) | +| extension_identifier | string | `sirsoft-ecommerce` | 이 리소스를 소유한 확장의 식별자 | +| is_active | boolean | `true` | active 여부 | +| is_default | boolean | `true` | default 여부 | +| user_overrides | null | `null` | 운영자가 수정한 필드명 목록 (예: ["name","is_active"]) | +| templates | array | `[{"id":1,"definition_id":1,"channel":"mail","subject":{"k…` | 템플릿 목록 (각 원소 identifier/name 등 — 템플릿 관계 파생) | +| 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":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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 흐름)" + }, + { + "key": "expire_minutes", + "description": "만료까지 남은 분" + }, + { + "key": "purpose_label", + "description": "인증 목적 라벨" + }, + { + "key": "app_name", + "description": "사이트명" + }, + { + "key": "site_url", + "description": "사이트 URL" + }, + { + "key": "recipient_email", + "description": "수신자 이메일" + } + ], + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "is_active": true, + "is_default": true, + "user_overrides": null, + "templates": [ + { + "id": 1, + "definition_id": 1, + "channel": "mail", + "subject": { + "ko": "[{app_name}] 결제 본인 확인 인증 코드", + "en": "[{app_name}] Checkout Verification Code" + }, + "body": { + "ko": "

결제 본인 확인

결제를 진행하기 위해 본인 확인이 필요합니다. 아래 인증 코드를 입력해 주세요.

{code}

이 코드는 {expire_minutes}분 후 만료됩니다.

본인이 결제를 진행하지 않았다면 이 메일을 무시하고 즉시 비밀번호를 변경해 주세요.

감사합니다,
{app_name}

", + "en": "

Checkout Verification

Please enter the code below to proceed with payment.

{code}

This code will expire in {expire_minutes} minutes.

If you did not initiate this payment, please ignore this email and change your password immediately.

Thank you,
{app_name}

" + }, + "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 10:43:32", + "abilities": { + "can_update": true, + "can_delete": false + } + } +} +``` **에러 응답** @@ -372,9 +861,108 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | definition | path | string | 예 | — | 대상 definition의 식별자 | +**요청 예시** + +```http +PATCH /api/admin/identity/messages/definitions/1/toggle-active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| provider_id | string | `g7:core.mail` | provider 식별자 (연관 리소스 참조) | +| scope_type | string | `purpose` | 메시지 정의 스코프 (provider_default\|purpose\|policy) — App\Enums\IdentityMessageScopeType enum | +| scope_value | string | `checkout_verification` | 범위 값: provider_default 빈 문자열 / purpose 키 / policy 키 | +| name | object | `{"ko":"결제 시 본인 확인","en":"Checkout Verification"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| description | object | `{"ko":"결제 진행 전 본인\/성인 확인 인증 코드 메일","en":"Identity\/adult …` | 설명 (다국어 필드는 로케일별 값 객체) | +| channels | array | `["mail"]` | 활성 채널 (현재 ["mail"], 향후 sms 등 확장) | +| variables | array | `[{"key":"code","description":"인증 코드 (text_code 흐름)"},{"ke…` | 사용 가능 변수 메타데이터 ([{key, description}]) | +| extension_type | string | `module` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) | +| extension_identifier | string | `sirsoft-ecommerce` | 이 리소스를 소유한 확장의 식별자 | +| is_active | boolean | `false` | active 여부 | +| is_default | boolean | `true` | default 여부 | +| user_overrides | array | `["is_active"]` | 운영자가 수정한 필드명 목록 (예: ["name","is_active"]) | +| templates | null | `null` | 템플릿 목록 (각 원소 identifier/name 등 — 템플릿 관계 파생) | +| 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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 흐름)" + }, + { + "key": "expire_minutes", + "description": "만료까지 남은 분" + }, + { + "key": "purpose_label", + "description": "인증 목적 라벨" + }, + { + "key": "app_name", + "description": "사이트명" + }, + { + "key": "site_url", + "description": "사이트 URL" + }, + { + "key": "recipient_email", + "description": "수신자 이메일" + } + ], + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "is_active": false, + "is_default": true, + "user_overrides": [ + "is_active" + ], + "templates": null, + "created_at": "2026-07-08 10:43:32", + "updated_at": "2026-07-08 12:14:36", + "abilities": { + "can_update": true, + "can_delete": false + } + } +} +``` **에러 응답** @@ -403,10 +991,32 @@ _단건 응답: `data` 객체의 필드._ | data | body | array | 아니오 | — | 데이터 페이로드 | | locale | body | string | 아니오 | max 10 | 로케일 코드 (표시 언어/지역) | +**요청 예시** + +```http +POST /api/admin/identity/messages/templates/preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "template_id": 1, + "data": [ + "예시값" + ], + "locale": "ko" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -437,10 +1047,34 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity.message_template.filter_update_rules`). +**요청 예시** + +```http +PATCH /api/admin/identity/messages/templates/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "subject": [ + "예시값" + ], + "body": [ + "예시값" + ], + "is_active": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -467,9 +1101,69 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | template | path | string | 예 | — | 대상 template의 식별자 | +**요청 예시** + +```http +POST /api/admin/identity/messages/templates/1/reset HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| definition_id | integer | `1` | definition 식별자 (연관 리소스 참조) | +| channel | string | `mail` | 메시지 템플릿 채널 (mail 현재 / sms 등 향후) — IdentityVerificationChannel 과는 별개의 도메인 분류 | +| subject | object | `{"ko":"[{app_name}] 결제 본인 확인 인증 코드","en":"[{app_name}] Ch…` | 다국어 제목 ({"ko":"...", "en":"..."}) — mail 채널에서만 의미 | +| body | object | `{"ko":"

결제 본인 확인<\/h1>

결제를 진행하기 위해 본인 확인이 필요합니다. 아래 …` | 다국어 본문 ({"ko":"...", "en":"..."}) | +| is_active | boolean | `true` | active 여부 | +| is_default | boolean | `true` | default 여부 | +| user_overrides | null | `null` | 운영자가 수정한 필드명 목록 (예: ["subject","body","is_active"]) | +| updated_by | null | `null` | 최종 수정한 사용자 정보 (uuid/name — updated_by 관계 파생, 없으면 null) | +| 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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": "

결제 본인 확인

결제를 진행하기 위해 본인 확인이 필요합니다. 아래 인증 코드를 입력해 주세요.

{code}

이 코드는 {expire_minutes}분 후 만료됩니다.

본인이 결제를 진행하지 않았다면 이 메일을 무시하고 즉시 비밀번호를 변경해 주세요.

감사합니다,
{app_name}

", + "en": "

Checkout Verification

Please enter the code below to proceed with payment.

{code}

This code will expire in {expire_minutes} minutes.

If you did not initiate this payment, please ignore this email and change your password immediately.

Thank you,
{app_name}

" + }, + "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 + } + } +} +``` **에러 응답** @@ -496,9 +1190,71 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | template | path | string | 예 | — | 대상 template의 식별자 | +**요청 예시** + +```http +PATCH /api/admin/identity/messages/templates/1/toggle-active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| definition_id | integer | `1` | definition 식별자 (연관 리소스 참조) | +| channel | string | `mail` | 메시지 템플릿 채널 (mail 현재 / sms 등 향후) — IdentityVerificationChannel 과는 별개의 도메인 분류 | +| subject | object | `{"ko":"[{app_name}] 결제 본인 확인 인증 코드","en":"[{app_name}] Ch…` | 다국어 제목 ({"ko":"...", "en":"..."}) — mail 채널에서만 의미 | +| body | object | `{"ko":"

결제 본인 확인<\/h1>

결제를 진행하기 위해 본인 확인이 필요합니다. 아래 …` | 다국어 본문 ({"ko":"...", "en":"..."}) | +| is_active | boolean | `false` | active 여부 | +| is_default | boolean | `true` | default 여부 | +| user_overrides | array | `["is_active"]` | 운영자가 수정한 필드명 목록 (예: ["subject","body","is_active"]) | +| updated_by | null | `null` | 최종 수정한 사용자 정보 (uuid/name — updated_by 관계 파생, 없으면 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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": "

결제 본인 확인

결제를 진행하기 위해 본인 확인이 필요합니다. 아래 인증 코드를 입력해 주세요.

{code}

이 코드는 {expire_minutes}분 후 만료됩니다.

본인이 결제를 진행하지 않았다면 이 메일을 무시하고 즉시 비밀번호를 변경해 주세요.

감사합니다,
{app_name}

", + "en": "

Checkout Verification

Please enter the code below to proceed with payment.

{code}

This code will expire in {expire_minutes} minutes.

If you did not initiate this payment, please ignore this email and change your password immediately.

Thank you,
{app_name}

" + }, + "is_active": false, + "is_default": true, + "user_overrides": [ + "is_active" + ], + "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 + } + } +} +``` **에러 응답** @@ -533,6 +1289,15 @@ _단건 응답: `data` 객체의 필드._ | search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | | per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +**요청 예시** + +```http +GET /api/admin/identity/policies?scope=%EC%98%88%EC%8B%9C%EA%B0%92&purpose=%EC%98%88%EC%8B%9C%EA%B0%92&source_type=%EC%98%88%EC%8B%9C%EA%B0%92&source_identifier=example-key&applies_to=%EC%98%88%EC%8B%9C%EA%B0%92&fail_mode=%EC%98%88%EC%8B%9C%EA%B0%92&enabled=1&search=%EC%98%88%EC%8B%9C%EA%B0%92&per_page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -558,6 +1323,81 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | updated_at | string | `2026-06-26 16:33:04` | 최종 수정 일시 | | abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "data": [ + { + "id": 12, + "key": "sirsoft-board.post.user_create", + "scope": "hook", + "target": "sirsoft-board.post.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 10:44:35", + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "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": 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 10:44:35", + "abilities": { + "can_update": true, + "can_delete": true + } + }, + "... (총 12건 중 2건 표시)" + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + }, + "meta": { + "current_page": 1, + "per_page": 25, + "total": 12, + "last_page": 1 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -596,10 +1436,41 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity_policy.store_validation_rules`). +**요청 예시** + +```http +POST /api/admin/identity/policies HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "key": "예시값", + "scope": "예시값", + "target": "예시값", + "purpose": "예시값", + "provider_id": "예시값", + "grace_minutes": 1, + "enabled": true, + "priority": 1, + "conditions": [ + "예시값" + ], + "applies_to": "예시값", + "fail_mode": "예시값", + "source_identifier": "example-key" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -627,10 +1498,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity_policy.destroy_validation_rules`). +**요청 예시** + +```http +DELETE /api/admin/identity/policies/{id} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -670,10 +1554,40 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity_policy.update_validation_rules`). +**요청 예시** + +```http +PUT /api/admin/identity/policies/{id} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "enabled": true, + "grace_minutes": 1, + "provider_id": "예시값", + "fail_mode": "예시값", + "key": "예시값", + "scope": "예시값", + "target": "예시값", + "purpose": "예시값", + "priority": 1, + "conditions": [ + "예시값" + ], + "applies_to": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -703,10 +1617,28 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity_policy.reset_field_validation_rules`). +**요청 예시** + +```http +POST /api/admin/identity/policies/{id}/reset-field HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "field": "enabled" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -731,6 +1663,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/identity/providers HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -744,6 +1685,50 @@ _요청 파라미터 없음._ | abilities | object | `{"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | | settings_schema | object | `{"code_length":{"label":"인증 코드 길이","type":"integer","defa…` | 관리자 설정 UI 반복 렌더용 설정 스키마 (필드별 label/type/default/options/help — 코드 길이·만료 시간 등) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": [ + { + "id": "g7:core.mail", + "label": "이메일", + "channels": [ + "email" + ], + "channel_labels": { + "email": "이메일" + }, + "render_hint": "text_code", + "is_available": true, + "abilities": { + "can_update": true + }, + "settings_schema": { + "code_length": { + "label": "인증 코드 길이", + "type": "integer", + "default": 6, + "help": "발송되는 숫자 코드의 자릿수 (기본 6, 최소 4, 최대 10)." + }, + "from_address": { + "label": "발신자 주소", + "type": "string", + "default": null, + "help": "비어 있으면 시스템 기본 발신자를 사용합니다." + } + } + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -773,10 +1758,32 @@ _요청 파라미터 없음._ | state | body | string | 아니오 | max 512 | 콜백 위변조 방지용 state 값 (요청 시 발급한 값과 대조) | | redirect_url | body | string | 아니오 | max 2048 | 인증 완료 후 되돌아갈 URL (open redirect 방지 위해 same-origin 만 허용) | +**요청 예시** + +```http +POST /api/identity/callback/{providerId} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +Content-Type: application/json + +{ + "challenge_id": "예시값", + "code": "예시값", + "token": "{YOUR_TOKEN}", + "state": "예시값", + "redirect_url": "https://example.com" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -805,10 +1812,32 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity.request_validation_rules`). +**요청 예시** + +```http +POST /api/identity/challenges HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +Content-Type: application/json + +{ + "purpose": "예시값", + "target": [ + "예시값" + ], + "provider_id": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -833,6 +1862,15 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | challenge | path | string | 예 | — | 대상 challenge의 식별자 | +**요청 예시** + +```http +GET /api/identity/challenges/{challenge} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -849,6 +1887,10 @@ _단건 응답: `data` 객체의 필드._ | max_attempts | integer | `5` | 허용 최대 시도 횟수 | | public_payload | array | `[]` | 프론트 렌더에 필요한 공개 안전 페이로드 (민감 metadata 제외, 프로바이더별 UI 힌트 — 없으면 빈 배열) | +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -872,10 +1914,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | challenge | path | string | 예 | — | 대상 challenge의 식별자 | +**요청 예시** + +```http +POST /api/identity/challenges/{challenge}/cancel HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -904,10 +1959,29 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity.verify_validation_rules`). +**요청 예시** + +```http +POST /api/identity/challenges/{challenge}/verify HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +Content-Type: application/json + +{ + "code": "예시값", + "token": "{YOUR_TOKEN}" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -934,10 +2008,23 @@ _단건 응답: `data` 객체의 필드._ | scope | query | string | 예 | max 32 | 조회 범위 한정 키 | | target | query | string | 예 | max 255 | 정책 매칭 대상 (scope 와 함께 해석 — 라우트명/URI 패턴, 훅 이름, 또는 custom key) | +**요청 예시** + +```http +GET /api/identity/policies/resolve?scope=%EC%98%88%EC%8B%9C%EA%B0%92&target=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -959,6 +2046,14 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/identity/providers HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -971,6 +2066,36 @@ _요청 파라미터 없음._ | is_available | boolean | `true` | available 여부 | | abilities | object | `{"can_update":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": [ + { + "id": "g7:core.mail", + "label": "이메일", + "channels": [ + "email" + ], + "channel_labels": { + "email": "이메일" + }, + "render_hint": "text_code", + "is_available": true, + "abilities": { + "can_update": false + } + } + ] +} +``` + **에러 응답** _대표 에러 없음 (공개 조회). 인증·권한 미요구 엔드포인트로 도메인 특이 에러를 반환하지 않습니다._ @@ -990,6 +2115,14 @@ _대표 에러 없음 (공개 조회). 인증·권한 미요구 엔드포인트 _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/identity/purposes HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -1002,6 +2135,82 @@ _요청 파라미터 없음._ | source_type | string | `core` | 목적 출처 (core: 코어 기본 4종 / module / plugin — 어느 확장이 선언했는지) | | source_identifier | string | `core` | 출처 식별자 (목적을 선언한 확장의 identifier) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": [ + { + "id": "signup", + "label": "회원가입 인증", + "description": "신규 가입자의 이메일/전화번호 소유 확인.", + "default_provider": "g7:core.mail", + "allowed_channels": [ + "mail", + "sms" + ], + "source_type": "core", + "source_identifier": "core" + }, + { + "id": "password_reset", + "label": "비밀번호 재설정", + "description": "비밀번호를 잊은 사용자가 본인 확인 후 재설정.", + "default_provider": "g7:core.mail", + "allowed_channels": [ + "mail", + "sms" + ], + "source_type": "core", + "source_identifier": "core" + }, + { + "id": "self_update", + "label": "자기 정보 변경", + "description": "로그인 사용자가 이메일/전화 등 본인 정보를 변경할 때.", + "default_provider": "g7:core.mail", + "allowed_channels": [ + "mail", + "sms" + ], + "source_type": "core", + "source_identifier": "core" + }, + { + "id": "sensitive_action", + "label": "민감 작업", + "description": "계정 탈퇴·관리자 작업 등 재인증이 필요한 시점.", + "default_provider": "g7:core.mail", + "allowed_channels": [ + "mail", + "sms" + ], + "source_type": "core", + "source_identifier": "core" + }, + { + "id": "checkout_verification", + "label": "결제 시 본인 확인", + "description": "결제 진행 전 성인/본인 확인이 필요한 경우 사용됩니다.", + "default_provider": null, + "allowed_channels": [ + "email", + "sms", + "ipin" + ], + "source_type": "module", + "source_identifier": "sirsoft-ecommerce" + } + ] +} +``` + **에러 응답** _대표 에러 없음 (공개 조회). 인증·권한 미요구 엔드포인트로 도메인 특이 에러를 반환하지 않습니다._ diff --git a/docs/backend/api/language-packs.md b/docs/backend/api/language-packs.md index 2e4a98a9..c00a6dfc 100644 --- a/docs/backend/api/language-packs.md +++ b/docs/backend/api/language-packs.md @@ -37,6 +37,15 @@ | per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | | page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +**요청 예시** + +```http +GET /api/admin/language-packs?scope=%EC%98%88%EC%8B%9C%EA%B0%92&target_identifier=example-key&locale=ko&status=%EC%98%88%EC%8B%9C%EA%B0%92&vendor=%EC%98%88%EC%8B%9C%EA%B0%92&search=%EC%98%88%EC%8B%9C%EA%B0%92&exclude_protected=1&per_page=1&page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -76,6 +85,120 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | has_update | boolean | `false` | update 여부 | | abilities | object | `{"can_activate":true,"can_deactivate":true,"can_uninstall…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "언어팩 목록을 조회했습니다.", + "data": { + "data": [ + { + "id": null, + "identifier": "g7-core-en", + "vendor": "g7", + "scope": "core", + "target_identifier": null, + "locale": "en", + "locale_name": "EN", + "locale_native_name": "English", + "text_direction": "ltr", + "version": "7.0.1", + "latest_version": null, + "target_version_constraint": null, + "target_version_mismatch": false, + "name": null, + "license": null, + "description": null, + "status": "active", + "is_protected": true, + "source_type": "built_in", + "origin": "built_in", + "source_url": "lang/en", + "github_url": null, + "github_changelog_url": null, + "bundled_identifier": null, + "install_blocked_reason": null, + "target_name": null, + "installed_at": null, + "activated_at": null, + "created_at": null, + "updated_at": null, + "has_update": false, + "abilities": { + "can_activate": true, + "can_deactivate": true, + "can_uninstall": true + } + }, + { + "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 + } + }, + "... (총 25건 중 2건 표시)" + ], + "meta": { + "total": 43, + "active": 15, + "installed": 0, + "inactive": 0, + "error": 0, + "uninstalled": 10, + "current_page": 1, + "last_page": 2, + "per_page": 25 + }, + "abilities": { + "can_install": true, + "can_activate": true, + "can_deactivate": true, + "can_uninstall": true, + "can_refresh_cache": true, + "can_check_updates": true, + "can_update": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -103,10 +226,30 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.language_packs.bulk_activate_validation_rules`). +**요청 예시** + +```http +POST /api/admin/language-packs/bulk-activate HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -130,10 +273,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/language-packs/check-updates HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -159,10 +315,29 @@ _요청 파라미터 없음._ | identifier | body | string | 예 | max 200 | 대상 확장/리소스의 식별자 | | auto_activate | body | boolean | 아니오 | — | 설치 후 자동 활성화 여부 | +**요청 예시** + +```http +POST /api/admin/language-packs/install-from-bundled HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "identifier": "example-key", + "auto_activate": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -189,10 +364,35 @@ _요청 파라미터 없음._ | file | body | file | 예 | max 10240 | 업로드 파일 | | auto_activate | body | boolean | 아니오 | — | 설치 후 자동 활성화 여부 | +**요청 예시** + +```http +POST /api/admin/language-packs/install-from-file HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary +Content-Disposition: form-data; name="auto_activate" + +1 +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -219,10 +419,29 @@ _요청 파라미터 없음._ | github_url | body | string | 예 | — | GitHub 저장소 URL | | auto_activate | body | boolean | 아니오 | — | 설치 후 자동 활성화 여부 | +**요청 예시** + +```http +POST /api/admin/language-packs/install-from-github HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "github_url": "https://example.com", + "auto_activate": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -250,10 +469,30 @@ _요청 파라미터 없음._ | checksum | body | string | 아니오 | — | 무결성 검증 체크섬 (SHA-256) | | auto_activate | body | boolean | 아니오 | — | 설치 후 자동 활성화 여부 | +**요청 예시** + +```http +POST /api/admin/language-packs/install-from-url HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "url": "https://example.com", + "checksum": "예시값", + "auto_activate": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -279,10 +518,31 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | file | body | file | 예 | max 5120 | 업로드 파일 | +**요청 예시** + +```http +POST /api/admin/language-packs/manifest-preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -306,10 +566,23 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/language-packs/refresh-cache HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -335,10 +608,23 @@ _요청 파라미터 없음._ | id | path | string | 예 | — | 대상 리소스의 식별자 | | cascade | query | boolean | 아니오 | — | 연쇄 처리 여부 (의존 항목 함께 처리) | +**요청 예시** + +```http +DELETE /api/admin/language-packs/1?cascade=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -365,9 +651,128 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/language-packs/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| identifier | string | `apidoc-sample-lang` | 언어팩 고유 식별자 ({vendor}-{scope}-{target?}-{locale}) | +| vendor | string | `apidoc` | 언어팩 제작자 식별자 | +| scope | string | `core` | 적용 대상 분류 | +| target_identifier | 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` | 텍스트 방향 | +| version | string | `1.0.0` | 언어팩 버전 | +| latest_version | string | `1.0.0` | 감지된 최신 배포 버전 | +| target_version_constraint | null | `null` | 대상 확장 버전 제약 (semver) | +| target_version_mismatch | boolean | `false` | 대상 버전 불일치 경고 플래그 | +| name | string | `API 문서 샘플 언어팩` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| license | string | `MIT` | 라이선스 | +| description | string | `문서 실측용 언어팩` | 설명 (다국어 필드는 로케일별 값 객체) | +| status | string | `active` | 상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조) | +| is_protected | boolean | `false` | protected 여부 | +| source_type | string | `bundled` | 설치 소스 유형 (zip/github/url/bundled/bundled_with_extension) | +| origin | string | `bundled` | 출처 (설치/등록 원천 구분 값) | +| source_url | null | `null` | source URL | +| github_url | null | `null` | GitHub 저장소 URL (manifest 파생) | +| github_changelog_url | null | `null` | GitHub 변경 이력(CHANGELOG) URL (manifest 파생) | +| bundled_identifier | null | `null` | 대응하는 번들 확장 식별자 (번들 원본 매칭용) | +| install_blocked_reason | null | `null` | 설치가 차단된 사유 (차단 없으면 null) | +| target_name | null | `null` | 대상 확장의 표시 이름 (scope+target_identifier 로 해석) | +| installed_at | string | `2026-07-05 10:41:24` | installed 일시 | +| activated_at | string | `2026-07-05 10:41:24` | activated 일시 | +| created_at | string | `2026-07-08 10:41:24` | 생성 일시 | +| updated_at | string | `2026-07-08 10:41:24` | 최종 수정 일시 | +| has_update | boolean | `false` | update 여부 | +| abilities | object | `{"can_activate":true,"can_deactivate":true,"can_uninstall…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +| manifest | object | `{"name":{"en":"API Doc Sample Pack","ko":"API 문서 샘플 언어팩"}…` | language-pack.json 전체 스냅샷 | +| validation_summary | object | `{"target_version_mismatch":false,"depends_on_core_locale"…` | 검증 결과 요약 객체 (통과/실패 건수 등) | +| source_meta | object | `{"type":"bundled","url":null,"installed_by":6,"latest_ver…` | 원천 메타데이터 객체 (출처·경로·해석 정보) | +| changelog_entries | array | `[]` | 변경 이력 항목 목록 (버전별 변경 내용) | + +**응답 예시** + +```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 + }, + "manifest": { + "name": { + "en": "API Doc Sample Pack", + "ko": "API 문서 샘플 언어팩" + }, + "locale": "fr", + "version": "1.0.0" + }, + "validation_summary": { + "target_version_mismatch": false, + "depends_on_core_locale": null + }, + "source_meta": { + "type": "bundled", + "url": null, + "installed_by": 6, + "latest_version": "1.0.0", + "directory_path": "lang-packs/apidoc-sample-lang" + }, + "changelog_entries": [] + } +} +``` **에러 응답** @@ -394,10 +799,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +POST /api/admin/language-packs/1/activate HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -423,9 +841,44 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/language-packs/1/changelog HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `apidoc-sample-lang` | 언어팩 고유 식별자 ({vendor}-{scope}-{target?}-{locale}) | +| entries | array | `[]` | 항목 목록 (각 원소는 대상 도메인 레코드) | +| changelog | string | `` | 변경 이력 텍스트 (원격/파일 CHANGELOG 본문) | +| has_changelog | boolean | `false` | changelog 여부 | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "언어팩 목록을 조회했습니다.", + "data": { + "identifier": "apidoc-sample-lang", + "entries": [], + "changelog": "", + "has_changelog": false + } +} +``` **에러 응답** @@ -452,10 +905,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +POST /api/admin/language-packs/1/deactivate HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -481,10 +947,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +POST /api/admin/language-packs/1/update HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/layouts.md b/docs/backend/api/layouts.md index 12457a03..15de5754 100644 --- a/docs/backend/api/layouts.md +++ b/docs/backend/api/layouts.md @@ -29,10 +29,23 @@ | --- | --- | --- | --- | --- | --- | | token | path | string | 예 | — | 인증/검증 토큰 | +**요청 예시** + +```http +GET /api/layouts/preview/{token}.json HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -57,10 +70,23 @@ | templateIdentifier | path | string | 예 | — | 대상 template의 식별자 | | layoutName | path | string | 예 | — | 대상 layout의 이름 (식별자) | +**요청 예시** + +```http +GET /api/layouts/{templateIdentifier}/{layoutName}.json HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/license.md b/docs/backend/api/license.md index 6647eb90..6f5f6524 100644 --- a/docs/backend/api/license.md +++ b/docs/backend/api/license.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/license HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -35,6 +44,22 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | content | string | `프로그램 명칭 : 그누보드7 (Gnuboard7) 저작자 : (주…` | 본문 내용 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "성공적으로 처리되었습니다.", + "data": { + "content": "프로그램 명칭 : 그누보드7 (Gnuboard7)\n\n저작자 : (주)에스아이알소프트\n\n라이선스 (License)\n\n번역문 아래에 원문이 있습니다.\n\n주의)\n1. 번역문과 원문의 내용상 차이가 있는 경우 원문의 내용을 우선으로 따릅니다.\n2. 이 라이선스 파일 및 내용은 저작자를 제외한 어느 누구도 추가, 수정, 삭제할 수 없습니다.\n\n----- MIT 라이선스 (한국어 번역) --------------------------------------------------------\n\nMIT 라이선스\n\nCopyright (c) 2026 (주)에스아이알소프트\n\n이 소프트웨어와 관련 문서 파일(이하 \"소프트웨어\")의 복사본을 취득하는 모든 사람에게\n소프트웨어를 제한 없이 사용, 복사, 수정, 병합, 출판, 배포, 서브라이선스 허여 및/또는\n판매할 수 있는 권리를 무상으로 부여합니다. 다만, 소프트웨어를 제공받은 사람은 다음\n조건을 따라야 합니다:\n\n위 저작권 고지와 본 허가 고지는 소프트웨어의 모든 복사본 또는 상당 부분에 포함되어야\n합니다.\n\n소프트웨어는 \"있는 그대로\" 제공되며, 명시적이든 묵시적이든 어떠한 종류의 보증도 하지\n않습니다. 여기에는 상품성, 특정 목적에의 적합성 및 비침해에 대한 보증이 포함되나 이에\n국한되지 않습니다. 어떠한 경우에도 저작자 또는 저작권자는 소프트웨어나 소프트웨어의\n사용 또는 기타 거래로 인해 발생하는 계약, 불법행위 또는 기타 청구, 손해 또는 기타\n책임에 대해 책임을 지지 않습니다.\n\n----- MIT License (English Original) --------------------------------------------------------\n\nThe MIT License (MIT)\n\nCopyright (c) 2026 SIRSOFT\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/locales.md b/docs/backend/api/locales.md index 3205d381..df4bc639 100644 --- a/docs/backend/api/locales.md +++ b/docs/backend/api/locales.md @@ -27,6 +27,14 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/locales/active HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -36,6 +44,31 @@ _단건 응답: `data` 객체의 필드._ | locales | array | `["ko","en","fr","ja"]` | 활성 로케일 코드 배열 | | locale_names | object | `{"ko":"한국어","en":"English","ja":"日本語","fr":"Français"}` | 로케일 코드별 표시명 맵 (config app.locale_names) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "locales.fetched", + "data": { + "locales": [ + "ko", + "en", + "fr" + ], + "locale_names": { + "ko": "한국어", + "en": "English", + "fr": "Français" + } + } +} +``` + **에러 응답** _대표 에러 없음 (공개 조회). 인증·권한 미요구 엔드포인트로 도메인 특이 에러를 반환하지 않습니다._ diff --git a/docs/backend/api/me.md b/docs/backend/api/me.md index df1624b1..c04f059b 100644 --- a/docs/backend/api/me.md +++ b/docs/backend/api/me.md @@ -27,10 +27,23 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +DELETE /api/me HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -54,6 +67,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/me HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -117,6 +139,57 @@ _단건 응답: `data` 객체의 필드._ | ecommerce_preferred_shipping_country | null | `null` | 선호 배송 국가 코드 (이커머스 모듈 주입, 미설정 시 null) | | ecommerce_preferred_shipping_country_name | null | `null` | 선호 배송 국가 이름 (코드 파생, 이커머스 모듈 주입, 미설정 시 null) | +**응답 예시** + +```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", + "avatar": null, + "language": "ko", + "timezone": "Asia/Seoul", + "country": "KR", + "status": "active", + "status_label": "활성", + "status_variant": "success", + "homepage": "https://example.com", + "mobile": "010-9595-2897", + "phone": "02-637-5618", + "zipcode": "16505", + "address": "경기도 안양시 봉은사로 2918", + "address_detail": "48동 718호", + "signature": "Fugit consequuntur repellendus sed.", + "bio": "Ut magni et sunt ducimus error adipisci. Pariatur corporis voluptatem ratione quo non saepe. Illo atque praesentium possimus dolores qui est fugit. Sint fugiat numquam voluptates.", + "is_super": false, + "is_admin": true, + "withdrawn_at": null, + "last_login_at": "2026-07-07 10:41:24", + "last_login_human": "1일 전", + "created_at": "2026-07-08 10:41:24", + "is_owner": 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 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -167,10 +240,47 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.update_profile_validation_rules`). +**요청 예시** + +```http +PUT /api/me HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": "예시 이름", + "nickname": "예시 이름", + "email": "user@example.com", + "password": "Password123!", + "current_password": "Password123!", + "language": "ko", + "country": "KR", + "timezone": "Asia/Seoul", + "homepage": "https://example.com", + "mobile": "010-1234-5678", + "phone": "010-1234-5678", + "zipcode": "06234", + "address": "서울특별시 강남구 테헤란로 1", + "address_detail": "서울특별시 강남구 테헤란로 1", + "signature": "예시값", + "bio": "예시 내용입니다.", + "notify_post_complete": true, + "notify_post_reply": true, + "notify_comment": true, + "notify_reply_comment": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/menus.md b/docs/backend/api/menus.md index f6fda8a0..54bdc242 100644 --- a/docs/backend/api/menus.md +++ b/docs/backend/api/menus.md @@ -34,6 +34,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.menu.list_validation_rules`). +**요청 예시** + +```http +GET /api/admin/menus?is_active=1&filters=%EC%98%88%EC%8B%9C%EA%B0%92&sort_by=created_at&sort_order=asc HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -58,6 +67,175 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "메뉴를 성공적으로 가져왔습니다.", + "data": { + "data": [ + { + "id": 19, + "name": { + "ko": "페이지 관리", + "en": "Page Management" + }, + "slug": "sirsoft-page", + "url": "/admin/pages", + "icon": "fas fa-file-alt", + "order": 25, + "is_active": true, + "parent_id": null, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "children": [], + "creator": null, + "roles": [ + { + "id": 1, + "name": { + "ko": "관리자", + "en": "Administrator" + }, + "permission_type": "read" + } + ], + "created_at": "2026-07-08 10:44:43", + "updated_at": "2026-07-08 10:44:43", + "is_owner": false, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + }, + { + "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", + "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": [ + { + "id": 1, + "name": { + "ko": "관리자", + "en": "Administrator" + }, + "permission_type": "read" + } + ] + }, + { + "id": 17, + "name": { + "ko": "게시판 목록", + "en": "Board List" + }, + "slug": "sirsoft-board-list", + "url": "/admin/boards", + "icon": "fas fa-list", + "order": 2, + "is_active": true, + "parent_id": 15, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "roles": [ + { + "id": 1, + "name": { + "ko": "관리자", + "en": "Administrator" + }, + "permission_type": "read" + } + ] + }, + { + "id": 18, + "name": { + "ko": "게시판 신고현황", + "en": "Board Reports" + }, + "slug": "sirsoft-board-reports", + "url": "/admin/boards/reports", + "icon": "fas fa-flag", + "order": 3, + "is_active": true, + "parent_id": 15, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "roles": [ + { + "id": 1, + "name": { + "ko": "관리자", + "en": "Administrator" + }, + "permission_type": "read" + } + ] + } + ], + "creator": null, + "roles": [ + { + "id": 1, + "name": { + "ko": "관리자", + "en": "Administrator" + }, + "permission_type": "read" + } + ], + "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 + } + }, + "... (총 3건 중 2건 표시)" + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -96,9 +274,121 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.menu.create_validation_rules`). +**요청 예시** + +```http +POST /api/admin/menus HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": "예시 이름", + "slug": "example-key", + "url": "https://example.com", + "icon": "예시값", + "parent_id": 1, + "order": 1, + "is_active": true, + "extension_type": "core", + "extension_identifier": "example-key", + "roles": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `26` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"실측 예시값","en":"실측 예시값","fr":"실측 예시값"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| slug | string | `probe_6a4dc09f5a6bd` | URL 친화 식별자 (slug) | +| url | string | `https://example.com` | 메뉴 URL | +| icon | string | `실측 예시값` | 아이콘 식별자 (아이콘 클래스/이름) | +| order | integer | `1` | 메뉴 순서 | +| is_active | boolean | `true` | active 여부 | +| parent_id | integer | `1` | parent 식별자 (연관 리소스 참조) | +| extension_type | string | `core` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) | +| extension_identifier | string | `probe_6a4dc09f5a6c3` | 이 리소스를 소유한 확장의 식별자 | +| parent | object | `{"id":1,"name":{"ko":"API 문서 샘플 메뉴","en":"API Doc Sample …` | 상위 항목 객체 (parent 관계 파생) | +| children | array | `[]` | 하위 항목 배열 (계층 트리 — children 관계 파생) | +| creator | null | `null` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| roles | array | `[{"id":1,"name":{"ko":"관리자","en":"Administrator"},"permis…` | 보유 역할 목록 (각 원소 id/name/permissions — roles 관계 파생) | +| created_at | string | `2026-07-08 12:14:39` | 생성 일시 | +| updated_at | string | `2026-07-08 12:14:39` | 최종 수정 일시 | +| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 201 +``` + +```json +{ + "success": true, + "message": "메뉴가 성공적으로 생성되었습니다.", + "data": { + "id": 26, + "name": { + "ko": "실측 예시값", + "en": "실측 예시값", + "fr": "실측 예시값" + }, + "slug": "probe_6a4dc09f5a6bd", + "url": "https://example.com", + "icon": "실측 예시값", + "order": 1, + "is_active": true, + "parent_id": 1, + "extension_type": "core", + "extension_identifier": "probe_6a4dc09f5a6c3", + "parent": { + "id": 1, + "name": { + "ko": "API 문서 샘플 메뉴", + "en": "API Doc Sample Menu" + }, + "url": "/admin/apidoc-sample", + "icon": "fas fa-book" + }, + "children": [], + "creator": null, + "roles": [ + { + "id": 1, + "name": { + "ko": "관리자", + "en": "Administrator" + }, + "permission_type": "read" + }, + { + "id": 4, + "name": { + "ko": "API 문서 샘플 역할", + "en": "API Doc Sample Role" + }, + "permission_type": "read" + } + ], + "created_at": "2026-07-08 12:14:39", + "updated_at": "2026-07-08 12:14:39", + "is_owner": false, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -125,6 +415,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/menus/active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -138,6 +437,272 @@ _요청 파라미터 없음._ | is_active | boolean | `true` | active 여부 | | children | array | `[]` | 하위 항목 배열 (계층 트리 — children 관계 파생) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "메뉴를 성공적으로 가져왔습니다.", + "data": [ + { + "id": 19, + "name": { + "ko": "페이지 관리", + "en": "Page Management" + }, + "slug": "sirsoft-page", + "url": "/admin/pages", + "icon": "fas fa-file-alt", + "order": 25, + "is_active": true, + "children": [] + }, + { + "id": 15, + "name": { + "ko": "게시판 관리", + "en": "Board Management" + }, + "slug": "sirsoft-board", + "url": null, + "icon": "fas fa-clipboard-list", + "order": 30, + "is_active": true, + "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" + }, + { + "id": 17, + "name": { + "ko": "게시판 목록", + "en": "Board List" + }, + "slug": "sirsoft-board-list", + "url": "/admin/boards", + "icon": "fas fa-list", + "order": 2, + "is_active": true, + "parent_id": 15, + "extension_type": "module", + "extension_identifier": "sirsoft-board" + }, + { + "id": 18, + "name": { + "ko": "게시판 신고현황", + "en": "Board Reports" + }, + "slug": "sirsoft-board-reports", + "url": "/admin/boards/reports", + "icon": "fas fa-flag", + "order": 3, + "is_active": true, + "parent_id": 15, + "extension_type": "module", + "extension_identifier": "sirsoft-board" + } + ] + }, + { + "id": 3, + "name": { + "ko": "이커머스", + "en": "Ecommerce" + }, + "slug": "sirsoft-ecommerce", + "url": null, + "icon": "fas fa-shopping-cart", + "order": 40, + "is_active": true, + "children": [ + { + "id": 4, + "name": { + "ko": "환경설정", + "en": "Settings" + }, + "slug": "sirsoft-ecommerce-settings", + "url": "/admin/ecommerce/settings", + "icon": "fas fa-cog", + "order": 1, + "is_active": true, + "parent_id": 3, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce" + }, + { + "id": 5, + "name": { + "ko": "상품 관리", + "en": "Products" + }, + "slug": "sirsoft-ecommerce-products", + "url": "/admin/ecommerce/products", + "icon": "fas fa-box", + "order": 2, + "is_active": true, + "parent_id": 3, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce" + }, + { + "id": 6, + "name": { + "ko": "카테고리 관리", + "en": "Categories" + }, + "slug": "sirsoft-ecommerce-categories", + "url": "/admin/ecommerce/categories", + "icon": "fas fa-folder", + "order": 3, + "is_active": true, + "parent_id": 3, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce" + }, + { + "id": 7, + "name": { + "ko": "브랜드 관리", + "en": "Brands" + }, + "slug": "sirsoft-ecommerce-brands", + "url": "/admin/ecommerce/brands", + "icon": "fas fa-tag", + "order": 4, + "is_active": true, + "parent_id": 3, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce" + }, + { + "id": 8, + "name": { + "ko": "상품정보제공고시", + "en": "Product Notice" + }, + "slug": "sirsoft-ecommerce-product-notices", + "url": "/admin/ecommerce/product-notices", + "icon": "fas fa-file-alt", + "order": 5, + "is_active": true, + "parent_id": 3, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce" + }, + { + "id": 9, + "name": { + "ko": "공통정보 관리", + "en": "Common Info" + }, + "slug": "sirsoft-ecommerce-common-info", + "url": "/admin/ecommerce/common-info", + "icon": "fas fa-info-circle", + "order": 6, + "is_active": true, + "parent_id": 3, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce" + }, + { + "id": 10, + "name": { + "ko": "주문 관리", + "en": "Orders" + }, + "slug": "sirsoft-ecommerce-orders", + "url": "/admin/ecommerce/orders", + "icon": "fas fa-receipt", + "order": 7, + "is_active": true, + "parent_id": 3, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce" + }, + { + "id": 11, + "name": { + "ko": "쿠폰 관리", + "en": "Coupons" + }, + "slug": "sirsoft-ecommerce-promotion-coupons", + "url": "/admin/ecommerce/promotion-coupons", + "icon": "fas fa-ticket-alt", + "order": 8, + "is_active": true, + "parent_id": 3, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce" + }, + { + "id": 12, + "name": { + "ko": "배송정책", + "en": "Shipping Policies" + }, + "slug": "sirsoft-ecommerce-shipping-policies", + "url": "/admin/ecommerce/shipping-policies", + "icon": "fas fa-truck", + "order": 9, + "is_active": true, + "parent_id": 3, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce" + }, + { + "id": 13, + "name": { + "ko": "리뷰 관리", + "en": "Reviews" + }, + "slug": "sirsoft-ecommerce-reviews", + "url": "/admin/ecommerce/reviews", + "icon": "fas fa-star", + "order": 10, + "is_active": true, + "parent_id": 3, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce" + }, + { + "id": 14, + "name": { + "ko": "마일리지 내역", + "en": "Mileage History" + }, + "slug": "sirsoft-ecommerce-mileage-transactions", + "url": "/admin/ecommerce/mileage-transactions", + "icon": "fas fa-coins", + "order": 11, + "is_active": true, + "parent_id": 3, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce" + } + ] + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -165,10 +730,23 @@ _요청 파라미터 없음._ | type | path | string | 예 | — | 확장 소유 타입 (ExtensionOwnerType 으로 파싱 — module: 모듈, plugin: 플러그인. 미유효 값이면 422 menu.invalid_extension_type) | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/menus/extension/{type}/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -194,6 +772,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/menus/hierarchy HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -209,6 +796,96 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | is_active | boolean | `true` | active 여부 | | children | array | `[]` | 하위 항목 배열 (계층 트리 — children 관계 파생) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "메뉴를 성공적으로 가져왔습니다.", + "data": { + "data": [ + { + "id": 19, + "name": { + "ko": "페이지 관리", + "en": "Page Management" + }, + "slug": "sirsoft-page", + "url": "/admin/pages", + "icon": "fas fa-file-alt", + "order": 25, + "is_active": true, + "children": [] + }, + { + "id": 15, + "name": { + "ko": "게시판 관리", + "en": "Board Management" + }, + "slug": "sirsoft-board", + "url": null, + "icon": "fas fa-clipboard-list", + "order": 30, + "is_active": true, + "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" + }, + { + "id": 17, + "name": { + "ko": "게시판 목록", + "en": "Board List" + }, + "slug": "sirsoft-board-list", + "url": "/admin/boards", + "icon": "fas fa-list", + "order": 2, + "is_active": true, + "parent_id": 15, + "extension_type": "module", + "extension_identifier": "sirsoft-board" + }, + { + "id": 18, + "name": { + "ko": "게시판 신고현황", + "en": "Board Reports" + }, + "slug": "sirsoft-board-reports", + "url": "/admin/boards/reports", + "icon": "fas fa-flag", + "order": 3, + "is_active": true, + "parent_id": 15, + "extension_type": "module", + "extension_identifier": "sirsoft-board" + } + ] + }, + "... (총 4건 중 2건 표시)" + ] + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -239,10 +916,36 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.menu.update_order_validation_rules`). +**요청 예시** + +```http +PUT /api/admin/menus/order HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "parent_menus": [ + "예시값" + ], + "child_menus": [ + "예시값" + ], + "moved_items": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -270,10 +973,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | menu | path | string | 예 | — | 대상 menu의 식별자 | +**요청 예시** + +```http +DELETE /api/admin/menus/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -301,6 +1017,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | menu | path | string | 예 | — | 대상 menu의 식별자 | +**요청 예시** + +```http +GET /api/admin/menus/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -326,6 +1051,67 @@ _단건 응답: `data` 객체의 필드._ | is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```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": [ + { + "id": 2, + "name": { + "ko": "하위 메뉴", + "en": "Child Menu" + }, + "slug": "apidoc-sample-menu-child", + "url": "/admin/nihil-non-eos-doloribus-occaecati-optio", + "icon": "fas fa-chart", + "order": 38, + "is_active": true, + "parent_id": 1, + "extension_type": null, + "extension_identifier": null, + "roles": [] + } + ], + "creator": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" + }, + "roles": [], + "created_at": "2026-07-08 10:41:24", + "updated_at": "2026-07-08 10:41:24", + "is_owner": true, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -365,10 +1151,39 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.menu.update_validation_rules`). +**요청 예시** + +```http +PUT /api/admin/menus/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": "예시 이름", + "slug": "example-key", + "url": "https://example.com", + "icon": "예시값", + "parent_id": 1, + "order": 1, + "is_active": true, + "extension_type": "core", + "extension_identifier": "example-key", + "roles": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -397,9 +1212,71 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | menu | path | string | 예 | — | 대상 menu의 식별자 | +**요청 예시** + +```http +PATCH /api/admin/menus/1/toggle-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 메뉴","en":"API Doc Sample Menu"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| slug | string | `apidoc-sample-menu` | URL 친화 식별자 (slug) | +| url | string | `/admin/apidoc-sample` | 메뉴 URL | +| icon | string | `fas fa-book` | 아이콘 식별자 (아이콘 클래스/이름) | +| order | integer | `35` | 메뉴 순서 | +| is_active | boolean | `false` | active 여부 | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| extension_type | null | `null` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) | +| extension_identifier | null | `null` | 이 리소스를 소유한 확장의 식별자 | +| created_at | string | `2026-07-08 10:41:24` | 생성 일시 | +| updated_at | string | `2026-07-08 12:14:40` | 최종 수정 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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": false, + "parent_id": null, + "extension_type": null, + "extension_identifier": null, + "created_at": "2026-07-08 10:41:24", + "updated_at": "2026-07-08 12:14:40", + "is_owner": true, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** diff --git a/docs/backend/api/modules.md b/docs/backend/api/modules.md index 692ec3c7..fa25de71 100644 --- a/docs/backend/api/modules.md +++ b/docs/backend/api/modules.md @@ -37,6 +37,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.index_validation_rules`). +**요청 예시** + +```http +GET /api/admin/modules?search=%EC%98%88%EC%8B%9C%EA%B0%92&filters=%EC%98%88%EC%8B%9C%EA%B0%92&status=installed&with=%EC%98%88%EC%8B%9C%EA%B0%92&per_page=1&page=1&include_hidden=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -64,6 +73,104 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | incompatible_required_version | null | `null` | 요구 코어 버전 미충족 시 필요한 버전 (호환되면 null) | | abilities | object | `{"can_install":true,"can_activate":true,"can_uninstall":t…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "모듈을 성공적으로 가져왔습니다.", + "data": { + "data": [ + { + "identifier": "sirsoft-board", + "vendor": "sirsoft", + "name": "게시판", + "version": "1.0.0", + "description": "게시판 관리를 위한 모듈", + "dependencies": [], + "status": "active", + "assets": null, + "update_available": false, + "update_source": null, + "latest_version": null, + "file_version": "1.0.0", + "github_url": "https://github.com/gnuboard/g7-module-sirsoft-board", + "github_changelog_url": "https://github.com/gnuboard/g7-module-sirsoft-board/releases", + "is_pending": false, + "is_bundled": false, + "deactivated_reason": null, + "deactivated_at": null, + "incompatible_required_version": null, + "abilities": { + "can_install": true, + "can_activate": true, + "can_uninstall": true + } + }, + { + "identifier": "sirsoft-ecommerce", + "vendor": "sirsoft", + "name": "이커머스", + "version": "1.0.1", + "description": "그누보드7 이커머스 모듈 - 상품, 주문, 결제 관리", + "dependencies": [], + "status": "active", + "assets": { + "js": "/api/modules/assets/sirsoft-ecommerce/dist/js/module.iife.js", + "css": "/api/modules/assets/sirsoft-ecommerce/dist/css/module.css", + "priority": 100 + }, + "update_available": false, + "update_source": null, + "latest_version": null, + "file_version": "1.0.1", + "github_url": "https://github.com/gnuboard/g7-module-sirsoft-ecommerce", + "github_changelog_url": "https://github.com/gnuboard/g7-module-sirsoft-ecommerce/releases", + "is_pending": false, + "is_bundled": false, + "deactivated_reason": null, + "deactivated_at": null, + "incompatible_required_version": null, + "abilities": { + "can_install": true, + "can_activate": true, + "can_uninstall": true + } + }, + "... (총 3건 중 2건 표시)" + ], + "pagination": { + "total": 3, + "current_page": 1, + "last_page": 1, + "per_page": 25 + }, + "meta": { + "total_modules": 3, + "active_modules": 0, + "system_modules": 0, + "user_modules": 3, + "total_installs": 0, + "average_rating": null, + "latest_version": "1.0.1", + "categories": [ + null + ], + "dependency_count": 0 + }, + "abilities": { + "can_install": true, + "can_activate": true, + "can_uninstall": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -92,10 +199,29 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.activate_validation_rules`). +**요청 예시** + +```http +POST /api/admin/modules/activate HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "module_name": "예시 이름", + "force": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -119,10 +245,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/modules/check-updates HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -150,10 +289,29 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.deactivate_validation_rules`). +**요청 예시** + +```http +POST /api/admin/modules/deactivate HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "module_name": "예시 이름", + "force": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -184,10 +342,35 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.install_validation_rules`). +**요청 예시** + +```http +POST /api/admin/modules/install HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "module_name": "예시 이름", + "vendor_mode": "auto", + "dependencies": [ + "예시값" + ], + "language_packs": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -215,10 +398,31 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.install_from_file_validation_rules`). +**요청 예시** + +```http +POST /api/admin/modules/install-from-file HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -246,10 +450,28 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.install_from_github_validation_rules`). +**요청 예시** + +```http +POST /api/admin/modules/install-from-github HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "github_url": "https://example.com" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -273,6 +495,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/modules/installed HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -300,6 +531,93 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | incompatible_required_version | null | `null` | 요구 코어 버전 미충족 시 필요한 버전 (호환되면 null) | | abilities | object | `{"can_install":true,"can_activate":true,"can_uninstall":t…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "모듈을 성공적으로 가져왔습니다.", + "data": { + "data": [ + { + "identifier": "sirsoft-board", + "vendor": "sirsoft", + "name": "게시판", + "version": "1.0.0", + "description": "게시판 관리를 위한 모듈", + "dependencies": [], + "status": "active", + "assets": null, + "update_available": false, + "update_source": null, + "latest_version": null, + "file_version": "1.0.0", + "github_url": "https://github.com/gnuboard/g7-module-sirsoft-board", + "github_changelog_url": "https://github.com/gnuboard/g7-module-sirsoft-board/releases", + "is_pending": false, + "is_bundled": false, + "deactivated_reason": null, + "deactivated_at": null, + "incompatible_required_version": null, + "abilities": { + "can_install": true, + "can_activate": true, + "can_uninstall": true + } + }, + { + "identifier": "sirsoft-ecommerce", + "vendor": "sirsoft", + "name": "이커머스", + "version": "1.0.1", + "description": "그누보드7 이커머스 모듈 - 상품, 주문, 결제 관리", + "dependencies": [], + "status": "active", + "assets": { + "js": "/api/modules/assets/sirsoft-ecommerce/dist/js/module.iife.js", + "css": "/api/modules/assets/sirsoft-ecommerce/dist/css/module.css", + "priority": 100 + }, + "update_available": false, + "update_source": null, + "latest_version": null, + "file_version": "1.0.1", + "github_url": "https://github.com/gnuboard/g7-module-sirsoft-ecommerce", + "github_changelog_url": "https://github.com/gnuboard/g7-module-sirsoft-ecommerce/releases", + "is_pending": false, + "is_bundled": false, + "deactivated_reason": null, + "deactivated_at": null, + "incompatible_required_version": null, + "abilities": { + "can_install": true, + "can_activate": true, + "can_uninstall": true + } + }, + "... (총 3건 중 2건 표시)" + ], + "meta": { + "total_modules": 3, + "active_modules": 0, + "system_modules": 0, + "user_modules": 3, + "total_installs": 0, + "average_rating": null, + "latest_version": "1.0.1", + "categories": [ + null + ], + "dependency_count": 0 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -323,10 +641,31 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | file | body | file | 예 | max 51200 | 업로드 파일 | +**요청 예시** + +```http +POST /api/admin/modules/manifest-preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -354,10 +693,28 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.refresh_layouts_validation_rules`). +**요청 예시** + +```http +POST /api/admin/modules/refresh-layouts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "module_name": "예시 이름" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -386,10 +743,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.uninstall_validation_rules`). +**요청 예시** + +```http +DELETE /api/admin/modules/uninstall?module_name=%EC%98%88%EC%8B%9C%20%EC%9D%B4%EB%A6%84&delete_data=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -413,6 +783,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/modules/uninstalled HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -440,6 +819,62 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | incompatible_required_version | null | `null` | 요구 코어 버전 미충족 시 필요한 버전 (호환되면 null) | | abilities | object | `{"can_install":true,"can_activate":true,"can_uninstall":t…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "모듈을 성공적으로 가져왔습니다.", + "data": { + "data": [ + { + "identifier": "gnuboard7-hello_module", + "vendor": "gnuboard7", + "name": "Hello 모듈", + "version": "0.1.0", + "description": "학습용 최소 샘플 모듈 (Memo CRUD)", + "dependencies": [], + "status": "uninstalled", + "assets": null, + "update_available": false, + "update_source": null, + "latest_version": null, + "file_version": null, + "github_url": null, + "github_changelog_url": null, + "is_pending": false, + "is_bundled": true, + "deactivated_reason": null, + "deactivated_at": null, + "incompatible_required_version": null, + "abilities": { + "can_install": true, + "can_activate": true, + "can_uninstall": true + } + } + ], + "meta": { + "total_modules": 1, + "active_modules": 0, + "system_modules": 0, + "user_modules": 1, + "total_installs": 0, + "average_rating": null, + "latest_version": "0.1.0", + "categories": [ + null + ], + "dependency_count": 0 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -469,9 +904,2219 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.extension.changelog_rules`). +**요청 예시** + +```http +GET /api/admin/modules/sirsoft-ecommerce/changelog?source=active&from_version=%EC%98%88%EC%8B%9C%EA%B0%92&to_version=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| changelog | array | `[{"version":"1.0.1","date":"2026-07-02","categories":[{"n…` | 변경 이력 텍스트 (원격/파일 CHANGELOG 본문) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "모듈을 성공적으로 가져왔습니다.", + "data": { + "changelog": [ + { + "version": "1.0.1", + "date": "2026-07-02", + "categories": [ + { + "name": "Fixed", + "items": [ + "관리자 주문 상세에서 주문상품을 \"구매확정\"으로 변경할 때 구매확정 처리가 고객이 직접 구매확정한 경우와 동일하게 이뤄지지 않던 문제를 수정했습니다 — 이제 구매확정 시점이 정확히 기록되어(부분 수량만 구매확정한 경우 포함), 구매확정 기준으로 지급되는 마일리지 적립이 정상적으로 지급되고, 리뷰 작성 기한도 올바르게 계산됩니다. 기존에는 관리자가 구매확정 처리한 주문상품이 마일리지 적립 대상에서 누락되고 리뷰 작성 기한이 계산되지 않을 수 있었습니다. (#61 @miles44229 님께서 제보해주셨습니다.)", + "주문상품을 여러 번에 나누어 구매확정할 때(예: 2개 중 1개씩 따로 확정) 적립 포인트가 일부만 지급되던 문제를 수정했습니다 — 이제 나누어 확정해도 예정 적립 포인트가 정확한 총액으로 지급됩니다.", + "마일리지 기능을 사용 안 함으로 설정한 뒤에는 지연 적립뿐 아니라 즉시 적립도 지급되지 않도록 통일했습니다 — 기존에는 지급 시점 설정에 따라 어떤 적립은 지급되고 어떤 적립은 지급되지 않을 수 있었습니다." + ] + } + ] + }, + { + "version": "1.0.0", + "date": "2026-07-01", + "categories": [ + { + "name": "Fixed", + "items": [ + "상품의 기본 통화와 다른 통화로 결제한 주문(예: 엔화로 등록된 상품을 원화로 결제)에서 결제·취소 금액의 통화 표기와 실제 환불 금액이 정확해졌습니다 — 주문서·주문 상세·결제 정보·취소 화면에서 금액은 주문 시점 기준 통화(상품 기본 통화)로 표기하고, 고객이 실제 결제한 통화와 그 통화 기준 청구액(예: \"결제 통화 KRW 4,750원 청구\")을 함께 표시합니다. 또한 주문을 취소·환불할 때 환불 금액이 고객이 실제 결제한 통화로 정확히 결제사에 요청되어, 기본 통화와 결제 통화가 다른 주문도 결제한 금액 그대로 환불됩니다(부분취소를 여러 번 나눠 진행해도 누적 환불액이 결제 통화 기준으로 정확히 계산됩니다). 이전에는 결제·취소 금액이 기본 통화 기호로만 고정 표기되고, 환불 요청 금액이 기본 통화 숫자로 전달되어 결제 통화와 단위가 어긋날 수 있었습니다.", + "상품의 기본 통화와 다른 통화로 결제할 때(예: 엔화로 등록된 상품을 원화로 결제) \"결제 금액이 일치하지 않습니다\"라는 오류로 결제가 막히던 문제를 수정했습니다 — 결제창에 청구되는 금액과 주문 승인 시 확인하는 금액의 통화 기준이 어긋나 있었습니다. 이제 결제창 청구액·승인 확인·입금 통보 확인이 모두 고객이 선택한 결제 통화 기준으로 일관되게 검증되어, 기본 통화와 결제 통화가 어떤 조합이든 정상적으로 결제가 진행됩니다.", + "상품에 배송정책을 따로 지정하지 않은 경우(\"기본 배송정책 사용\")에도 기본 배송정책이 그대로 적용됩니다 — 기존에는 배송정책을 지정하지 않은 상품이 해외 배송국가(예: 일본)를 선택하면 기본 배송정책에 그 국가가 설정돼 있어도 \"배송할 수 없는 상품\"으로 안내되고 배송비가 무료로 잡히던 문제가 있었습니다. 이제 상품 상세·장바구니·주문서·주문 처리·배송비 계산 전반에서 기본 배송정책의 국가별 설정과 배송비가 일관되게 반영됩니다. 주문 시점의 기본 배송정책은 주문에 함께 보관되어, 이후 기본 배송정책이 바뀌어도 취소·환불 배송비는 주문 당시 기준으로 계산됩니다.", + "관리자 대시보드의 이커머스 위젯(최신 리뷰·미답변 문의 목록)에서 각 항목이 같은 화면 식별자를 공유해 일부 보조 기능·접근성 도구가 항목을 구분하지 못하던 문제를 수정했습니다 — 이제 목록의 각 행이 고유하게 식별됩니다.", + "헤더의 통화·배송국가 선택기가 데스크톱과 모바일 화면에 동시에 배치되면서 같은 화면 식별자를 공유하던 문제를 수정했습니다 — 두 위치의 선택기가 각각 고유하게 식별됩니다." + ] + }, + { + "name": "Changed", + "items": [ + "상품 등록 화면에서 새로 시작할 때 과세 여부는 \"과세\", 판매상태는 \"판매중\", 전시상태는 \"전시중\"이 기본으로 선택되어 있고, 옵션을 추가하면 재고가 1로 미리 채워집니다 — 가장 흔한 설정이 처음부터 지정되어 매번 직접 고르거나 입력하는 수고를 덜 수 있습니다. 옵션을 일괄 생성하거나 행을 추가·삭제하면 원 상품의 재고가 옵션 재고 합계로 즉시 맞춰집니다(옵션 보유 상품의 재고는 직접 입력하지 않고 옵션 재고 합으로 표시됩니다) — 이전에는 옵션을 만든 직후에는 상품 재고가 0으로 남아 있다가 옵션 재고를 한 번 직접 수정해야 반영되었습니다. 기존 상품을 수정할 때는 저장된 값이 그대로 표시됩니다.", + "상품 등록·수정 화면과 상품 관리 목록에서 옵션별 외화 가격을 직접 입력하던 칸을 없애고, 판매가 아래에 환율로 자동 계산된 외화 가격을 읽기 전용으로 보여주도록 바꿨습니다. 외화 가격은 기본 통화 판매가를 기준으로 환율을 적용해 표시되므로(장바구니·주문서와 동일한 방식), 판매가만 입력하면 설정한 통화들의 환산 금액이 함께 표시됩니다. 표시되는 통화는 쇼핑몰 설정에서 추가한 통화를 그대로 따르며, 통화를 추가하면 자동으로 함께 나타납니다. 상품 관리 목록의 통화별 열과 열 선택 메뉴의 통화 항목, \"다중통화 가격 자동계산\" 켜기/끄기도 더 이상 필요하지 않아 정리했습니다. 판매가 아래에 외화 환산값이 여러 줄로 표시되어도 정가·재고·SKU 등 다른 입력칸이 같은 줄에 가지런히 정렬되도록 행 정렬을 맞췄습니다.", + "쇼핑몰 설정의 마일리지 알림 카드에서 사이트 알림 / 이메일 아이콘이 채널 라벨 위로 올라가 비대칭으로 보이던 회귀를 수정했습니다 — 아이콘이 채널 라벨 옆 가운데로 정렬되어 한 줄로 응집됩니다.", + "쿠폰 등록 / 수정 화면의 직접발급 안내문에서 정보 아이콘이 안내 문구 위로 살짝 떠 보이던 정렬을 바로잡았습니다 — 아이콘이 안내 문구와 세로 가운데로 맞춰집니다.", + "상품 등록 / 수정 화면에서 상단 탭 내비와 본문 콘텐츠가 너무 붙어 보이던 회귀를 수정했습니다 — 탭 내비와 첫 카드 사이에 표준 상단 여백이 자동으로 들어가 다른 관리자 화면과 동일한 결로 통일됩니다.", + "관리자 환경설정·상품·주문·배송정책·쿠폰 화면의 카드 모양과 간격을 다른 관리자 화면과 동일하게 통일했습니다 — 사용자 영역용 카드 자산이 혼용되던 부분을 관리자 표준 카드로 환원하고, 카드 안의 헤더 묶음·본문 묶음을 표준 패턴으로 정리했습니다.", + "관리자 환경설정 페이지의 빈 컨테이너 한 겹을 제거해 다른 관리자 화면과 본문 구조가 동일합니다 — 저장 중 시각 피드백(저장 버튼 안의 회전 아이콘과 \"저장 중...\" 문구)은 그대로 제공됩니다.", + "상품·카테고리의 SEO 제목·SEO 설명을 언어별로 입력할 수 있습니다 — 상품명·상세설명과 동일하게 언어 탭으로 한국어·영어 등 각 언어의 SEO 문구를 따로 작성할 수 있고, 검색엔진이 해당 언어로 접근하면 그 언어의 제목·설명이 노출됩니다. \"상품명과 동일하게 설정\"을 켜면 입력한 언어별 상품명·설명이 그대로 SEO에 반영되고, SEO 문구를 비워 두면 상품명·상세설명으로 자동 대체됩니다. 기존에 입력해 둔 SEO 문구는 기본 언어 값으로 보존됩니다. (이전에는 한 줄 문구만 저장되어 모든 언어에 같은 값이 쓰였고, 입력 중 검색결과 미리보기가 매 글자마다 다시 그려져 타이핑이 느려지던 문제도 함께 해소되었습니다.)", + "배송정책의 통화가 상품 등록과 동일하게 쇼핑몰 기본 통화로 자동 적용됩니다 — 배송정책 등록·수정 화면의 통화 항목이 직접 고르는 칸에서 현재 기본 통화를 보여 주는 읽기 전용 표시로 바뀌었고, 배송비는 항상 기본 통화 기준으로 계산됩니다. 배송비·무료배송 기준금액 입력칸 옆의 통화 표시도 기준 통화 칸과 동일하게 현재 기본 통화로 통일했습니다. 기존에 다른 통화로 저장돼 있던 배송정책도 기본 통화로 통일됩니다. (이전에는 배송정책마다 통화를 따로 지정할 수 있었으나 배송비 계산에는 반영되지 않아 합계가 어긋날 수 있었습니다.)", + "주문서 작성과 장바구니의 배송비가 선택한 배송 국가 기준으로 계산되어 표시됩니다 — 배송정책에 국가별 배송비(예: 국내·미국)를 다르게 설정한 경우, 헤더에서 고른 배송 국가 또는 주문서에서 선택한 배송 국가에 맞는 배송비가 적용됩니다. 주문서에서는 헤더에서 고른 국가가 기본으로 선택되며, 주문서의 배송 국가를 바꾸면 그 즉시 해당 국가 배송비로 다시 계산됩니다. 우편번호를 입력하지 않아도 국가만으로 해당 국가의 기본 배송비가 미리 표시됩니다. (이전에는 미국 등 해외 국가를 선택해도 배송비가 국내 기준으로만 계산되어 표시되던 문제가 있었습니다.)", + "상품 등록·수정 화면의 목록·복사·삭제·저장 버튼을 화면 하단 고정으로 옮기고, 환경설정 화면의 상단 탭과 저장 버튼이 스크롤 중에도 고정되도록 개선했습니다 — 긴 폼을 스크롤하는 동안에도 주요 버튼이 항상 화면에 보입니다. 배송정책·쿠폰·상품·주문 상세 화면에서 페이지 제목이 함께 따라 고정되던 동작은 일반 흐름으로 정리했습니다.", + "카테고리·브랜드 슬러그 수정 모드와 주문 상세의 자동 채움·참조용 입력 칸의 글자를 옅은 회색으로 표시해, 수정할 수 없는 항목임을 한눈에 구분할 수 있습니다.", + "이커머스 관리 전반(상품·주문·결제·배송·환경설정·카테고리·브랜드·쿠폰·마일리지·예치금·리뷰)의 카드·입력 칸·라벨·본문 텍스트·구분선·여백 모양을 다른 관리자 화면과 동일한 결로 통일했습니다 — 화면마다 조금씩 다르게 보이던 카드 모양·간격, 입력 칸 테두리·배경, 글자 톤, 2단 그리드 간격이 한 가지 기준으로 정합되고, 마일리지/예치금 환경설정의 페이지 배경이 누락되어 다르게 보이던 회귀도 함께 해소됩니다.", + "이커머스 관리 화면 곳곳의 역할 없는 빈 컨테이너 중첩을 정리해, 화면 모양은 그대로 둔 채 페이지 구조만 단순해졌습니다.", + "주문 결제 진입이 특정 결제사에 고정되지 않고, 주문을 만들 때 서버 응답이 알려주는 결제 수단으로 진행됩니다 — 결제 플러그인을 바꾸거나 새로 추가해도 주문 화면 수정 없이 해당 결제사의 결제창이 열립니다. 결제 수단을 따로 지정하지 않은 결제(무통장 입금 등)는 기존과 동일하게 동작합니다.", + "코어 최소 요구 버전을 7.0.0-beta.8 로 상향. SEO 자동값 연결 칩 편집 기능이 코어 7.0.0-beta.8 에서 제공하는 편집기 연동 표면을 사용합니다." + ] + }, + { + "name": "Added", + "items": [ + "레이아웃 편집기에서 마일리지 거래내역·쿠폰 목록·쿠폰 생성·주문 목록·주문 상세 화면의 화면 상태를 미리보기로 전환할 수 있습니다 — 목록의 비어 있음, 쿠폰 신규 작성·검증 실패, 주문 상세의 부분 취소 등 특정 상황에서만 나타나는 모양을 편집기에서 골라 미리보고 편집할 수 있습니다.", + "비회원으로 주문하면 주문 접수·입금 확인·배송·취소 등 주요 단계에서 입력한 이메일로 주문 알림이 발송됩니다 — 회원 주문과 동일한 알림을 비회원도 받으며, 메일은 주문할 때 보던 화면 언어로 전달됩니다. 주문 완료 화면에 \"주문 확인 안내를 입력하신 이메일로 보내드렸습니다\"라는 안내가 함께 표시됩니다.", + "회원이 자신의 배송국가를 직접 설정할 수 있습니다 — 해외배송을 켜면 사이트 상단 헤더의 통화 선택기에 배송국가 선택이 함께 표시되고(선택기 버튼에 배송국가 코드와 통화가 나란히 표시), 회원정보 수정·마이페이지·가입 화면에서도 배송국가를 고를 수 있습니다. 로그인한 회원은 선택한 배송국가가 계정에 저장되어 다음 방문에도 유지되고, 비회원은 브라우저에 임시 저장됩니다. 처음 방문 시에는 접속 위치를 추정해 기본 배송국가가 정해지며, 추정이 어려우면 설정한 기본 국가가 사용됩니다. 주문서·주문 상세·주문 내역·배송지 관리에 배송국가가 함께 표시됩니다(국내 포함). 선택한 배송국가로 배송할 수 없는 상품이 장바구니·주문서에 하나라도 있으면 그 상품이 표시되고 주문하기가 막히며, 상품 상세에서도 배송 가능 여부를 미리 확인할 수 있습니다. 해외 주소로 주문하면 도시·우편번호 등 해외 주소가 온전히 저장되고, 주문 알림 메일에도 받는 분·배송국가·배송지가 표시됩니다. 주문을 취소·환불할 때도 그 시점의 배송국가·우편번호가 함께 보관되어, 이후 배송지가 바뀌어도 도서산간·국가별 배송비 환불이 주문 당시 기준으로 정확히 계산됩니다. 해외배송을 끄면 배송국가 선택이 함께 사라집니다.", + "사이트 상단 헤더에 통화 선택기를 추가했습니다 — 이커머스 모듈을 켜면 헤더에 통화 전환 버튼이 나타나고, 관리자가 등록한 통화(환율이 설정된 통화) 중에서 표시 통화를 고를 수 있습니다. 로그인한 회원은 선택한 통화가 계정에 저장되어 다음 방문에도 유지되고, 비회원은 브라우저에 임시 저장됩니다. 관리자 화면 헤더에서도 동일하게 표시 통화를 전환할 수 있습니다. 모듈을 끄면 통화 선택기가 함께 사라집니다. 레이아웃 편집기 미리보기에서도 헤더의 통화·배송국가 선택기가 실제 화면과 동일하게 표시되어, 편집 중 위치와 모양을 미리 확인할 수 있습니다(사용자·관리자 템플릿 공통).", + "기본 통화가 원(KRW)이 아닌 통화(달러·유로 등)일 때도 상품을 등록·수정할 수 있습니다 — 기본 통화가 소수점을 쓰는 통화이면 상품 정가·판매가·옵션 가격과 추가옵션 추가금을 3.30 처럼 소수점까지 입력할 수 있고, 통화별 소수 자릿수(예: 달러 2자리)를 초과하는 입력은 막습니다. 원·엔처럼 소수점이 없는 통화에서는 정수만 허용됩니다. 기존에는 가격이 정수로만 저장되어 기본 통화를 달러 등으로 바꾸면 새 상품 저장이 되지 않았습니다. 아울러 상품·옵션·주문·결제·환불·쿠폰·마일리지·배송비·대시보드 매출 등 금액이 표시되는 화면 전반에서, 소수점이 없는 통화(원·엔 등)인데도 `200.00`처럼 불필요한 소수점이 붙던 부분을 정리해 — 모든 금액이 쇼핑몰 설정의 통화별 소수 자릿수를 그대로 따라 표시됩니다(소수점 없는 통화는 `200`, 달러·유로 등은 `1.70`).", + "기본 통화가 원(KRW)이 아닌 통화(달러 등)일 때 상품·장바구니·주문의 외화 환산 금액이 0 또는 비정상값으로 표시되던 문제를 해결했습니다 — 통화마다 \"환율 입력의 기준 단위\"(예: 원은 1,000원당, 달러·엔 등은 1단위당)를 설정에 두어, 어떤 통화를 기본으로 삼든 외화 가격이 올바르게 환산됩니다. 환율/통화 설정 화면에서 통화마다 \"기준 단위\"를 직접 입력·수정할 수 있고, 환율 입력 안내도 기본 통화의 기준 단위에 맞춰 \"USD 1 =\", \"KRW 1,000 =\"처럼 표시됩니다. 기준 단위·환율을 잘못 입력해 저장하면 해당 입력칸이 빨갛게 강조되고 칸 아래에 사유가 표시됩니다. 이미 접수된 주문의 환불 금액은 주문 당시 환율 그대로 유지됩니다.", + "상품 등록·수정 시 입력 오류 안내에 \"판매가\"·\"옵션 판매가\"처럼 사람이 읽을 수 있는 항목 이름이 표시됩니다 — 기존에는 일부 항목에서 내부 식별자(예: options.0.selling_price)가 그대로 노출됐습니다.", + "쇼핑몰 설정의 언어·통화 화면에서 통화마다 표시 기호를 직접 지정할 수 있습니다 — 통화 추가·수정 시 \"기호\" 칸에 ₩·$·¥·€ 등 원하는 기호를 입력하면, 상품 등록·수정 화면의 기본 통화 표시와 판매가 일괄 변경 창의 금액 단위, 상품 복사 미리보기의 가격이 모두 그 기호로 표시됩니다. 기존 통화(원·달러·엔·위안·유로)에는 표준 기호가 기본으로 채워지며, 통화를 추가·삭제하거나 기본 통화를 바꾸면 상품 관리 화면의 기호 표시가 설정을 그대로 따라갑니다. 위안화(CNY)는 엔화(JPY)와 같은 ¥ 기호를 쓰면 가격을 혼동할 수 있어 元 기호를 기본으로 사용합니다 — 장바구니·상품·주문 화면에서 엔화는 ¥, 위안화는 元 로 구분되어 표시됩니다. 아울러 장바구니·상품·주문서·주문 상세·주문 내역·영수증·배송정책 등 금액이 표시되는 화면 전반에서 통화 기호가 원화로 고정되어 있던 부분을 정리해, 기본 통화를 달러·엔 등으로 바꾸면 그 통화 기호로 표시됩니다(상품 옵션 추가금·과세금액 표기 포함). 이미 접수된 주문의 금액은 주문 시점의 통화로 계속 표시되어, 이후 기본 통화를 바꿔도 과거 주문 표기가 달라지지 않습니다. (이전에는 상품 관리 일부 화면이 통화 설정과 무관하게 원화 기호로 고정 표시되었고, 엔화와 위안화가 모두 ¥ 로 표시되어 구분되지 않았습니다.)", + "관리자 대시보드에 이커머스 영역이 추가되었습니다 — 이커머스 모듈을 켜면 상단에 상품·주문·리뷰·쿠폰·배송 바로가기 버튼이 나타나고, 오늘 주문 현황(결제완료·준비중·배송중·취소 등) 배지, 최근 7일 판매 추세 그래프(판매 수량·매출액과 직전 7일 대비 증감율), 최신 리뷰, 미답변 문의 위젯이 실제 데이터로 표시됩니다. 모듈을 끄면 이 영역과 버튼이 함께 사라집니다.", + "대시보드 판매 현황은 매시간 집계되어 빠르게 표시됩니다 — 판매 수량과 매출은 결제 완료 이후(배송보류·준비중·배송준비·배송중·배송완료·구매확정) 주문상품을 기준으로 집계하며, 취소된 수량은 자동으로 차감되고 주문일 기준으로 날짜에 반영됩니다.", + "마이페이지 취소 주문 상세에서 \"재주문\" 기능 추가 — 과거 주문의 상품 옵션을 현재 장바구니에 일괄 추가, 품절/단종 상품은 사유와 함께 안내", + "비회원 주문 지원 — 로그인 없이 주문/결제하고, 주문번호·휴대폰·조회 비밀번호(8자 이상)로 본인 확인 후 30분간 유효한 토큰으로 주문을 조회할 수 있습니다. 회원/비회원은 주문 단계에서 자동 분기됩니다.", + "비회원 주문 상세에서 주문 취소·환불 예상·구매확정과 배송 전 배송지 변경(직접 입력)을 회원과 동일하게 이용할 수 있으며, 상품 합계·할인·배송비·총액의 다중 통화 금액도 함께 표시됩니다.", + "상품 \"구매 대상 제한\"을 실제 주문 단계에 적용 — 허용 역할에 없는 사용자(비회원 포함)는 제한 상품을 주문할 수 없으며, 주문서 진입과 결제 시점 양쪽에서 안내됩니다. 기존에는 설정만 저장되고 구매를 막지 않았습니다.", + "관리자가 주문 목록에서 회원/비회원을 구분해 조회하고, 비회원 주문의 조회 비밀번호를 재설정할 수 있습니다.", + "관리자 주문 상세의 주문상품 표에서 상품별 재고 차감 여부(\"재고 차감\"/\"재고 미차감\")를 구매수량과 함께 확인할 수 있습니다.", + "주문이 취소된 경우 관리자 주문 상세에 취소 사유가 표시됩니다 — 취소 유형(전체/부분)과 사유, \"기타\" 선택 시 입력한 상세 사유, 취소 일시를 취소 건별로 확인할 수 있습니다. 부분 취소가 여러 번 이뤄진 주문은 각 취소 건이 최근 순으로 모두 나열됩니다.", + "상품·장바구니·주문·찜·문의 등 쇼핑 화면을 샘플 데이터로 미리보고 편집할 수 있습니다 — 재고 있음/품절, 목록 있음/없음, 장바구니 비었음 같은 화면 상태도 미리보기로 전환해 볼 수 있습니다.", + "화면 상태 미리보기를 더 많은 쇼핑 화면으로 넓혔습니다 — 상품의 품절·판매 중지·판매 예정, 상품 목록의 접근 제한, 장바구니 주문 처리 중, 결제 오류, 주문 완료의 결제수단별 안내(카드·가상계좌·무통장), 재주문 진행/일부/실패와 함께, 관리자의 상품·쿠폰·배송정책 폼(신규/수정/검증 실패)과 쇼핑몰 설정의 모든 탭(기본 정보·언어 통화·SEO·주문·취소 반품·배송·리뷰·알림 정의·본인인증)·마일리지/예치금 탭을 전환해 미리볼 수 있습니다.", + "주문서(결제) 화면의 입력 검증 실패 상태를 미리보기에 추가 — 주문자 이름·연락처·이메일, 받는 분 이름·연락처, 우편번호·주소 입력칸에 빨간 오류 안내가 표시된 화면을 전환해 미리볼 수 있습니다.", + "비회원 주문 조회 화면을 회원/비회원 상태로 전환해 미리볼 수 있습니다 — 비회원 상태에서는 주문 조회 입력 폼을, 회원 상태에서는 마이페이지 안내를 각각 편집할 수 있습니다.", + "데이터 소스 목록에서 이 확장이 제공하는 데이터 소스가 친화 명칭으로 표시되고, 어느 확장이 제공했는지 출처가 함께 표시됩니다.", + "[검색엔진] 탭에서 상품·카테고리의 공유 이미지·제목·구조화 데이터 등 이 확장이 제공하는 SEO 자동값이 \"상품 이름\"·\"상품 대표 이미지\" 같은 데이터 연결 칩(출처 표시 포함)으로 보이고, 다른 데이터로 바꿔 지정할 수 있습니다.", + "주문 구매확정 시 결제 금액에 적립률을 적용해 마일리지를 자동 적립하고, 결제 시 보유 마일리지를 사용할 수 있습니다. 적립·사용·소멸 내역이 회원별로 원장에 기록됩니다.", + "마일리지로 결제 금액 전액을 충당하면 별도 결제 절차 없이 주문이 즉시 결제완료 처리됩니다.", + "마일리지 차감 시점을 결제수단별로 \"결제완료 시\" 또는 \"주문접수 시\" 중에서 선택할 수 있습니다. 무통장입금·가상계좌는 입금 전 마일리지가 재사용되지 않도록 주문접수 시 차감이 기본이고, 신용카드(PG)는 결제창을 닫거나 결제에 실패한 주문에서 마일리지가 선차감되어 사라지지 않도록 결제완료 시 차감이 기본입니다. 차감된 뒤 결제가 실패하거나 주문을 취소한 경우에는 사용 마일리지가 자동으로 복원됩니다. 마일리지 사용을 꺼 두면 결제수단별 차감 시점 설정은 비활성화됩니다.", + "마일리지 사용 시 먼저 적립된 분부터 차감(선입선출)되며, 주문 취소·부분 취소 시 사용분 복원과 적립 회수가 정확히 재계산됩니다.", + "무통장입금 주문 접수 시 마일리지 차감 후 실제 입금할 금액과 입금 계좌·기한을 담은 입금 안내가 자동 발송됩니다.", + "통화별 마일리지 규칙(적립률·사용 한도·최소 사용 금액·소멸 기간)을 설정할 수 있으며, 다중 통화 환경에서 통화별로 독립 적용됩니다.", + "적립 마일리지에 유효기간을 두어 만료분이 자동 소멸하며, 소멸 예정 마일리지를 회원에게 사전 안내할 수 있습니다.", + "회원 탈퇴 시 보유 마일리지가 정리되어 잔여가 남지 않습니다.", + "환경설정에 \"마일리지\" 탭이 추가되어 기능 사용 여부·기본 적립률·통화별 규칙·유효기간·소멸 알림을 한 화면에서 관리합니다. 통화 추가가 미확정인 상태로는 저장이 차단되고, 적립률 0%·잘못된 통화 코드 등 잘못된 입력은 저장 전 안내됩니다.", + "관리자 \"마일리지 내역\" 화면에서 회원·유형·통화·기간으로 거래를 조회하고, 특정 회원에게 마일리지를 수동 지급/차감하거나 선택한 적립분의 유효기간을 일괄 연장할 수 있습니다. 다른 관리자 목록 화면(주문/리뷰)과 동일한 필터·페이지네이션·표 구조로 정렬했습니다.", + "각 적립 내역의 \"사유·기간 변경\" 메뉴에서 적립 사유와 만료일을 수정할 수 있습니다. 회원·금액·유형·적립일은 변경할 수 없도록 함께 표시되며, 이미 소멸되었거나 모두 사용된 적립분은 만료일을 바꿀 수 없습니다.", + "마일리지 내역 표에 \"유효기간\" 열이 추가되어 적립분의 만료 예정일, 무기한 지급 여부, 그리고 소멸된 경우 \"전체 소멸\"/\"일부 소멸\" 여부와 소멸된 금액을 한눈에 확인할 수 있습니다.", + "거래 행을 펼치면 표시되는 연결 거래에 관련 주문번호(클릭 시 주문 상세 열람)와 지급한 관리자(또는 시스템 발생) 정보가 함께 표시됩니다.", + "수동 지급 시 적립분의 유효기간을 \"정책 따름\" 또는 \"직접 지정\"(날짜 선택) 중에서 명확한 안내와 함께 선택할 수 있습니다.", + "주문 관련 모든 화면에서 마일리지 사용액·적립 예정액이 일관되게 표시됩니다 — 주문서 결제 요약, 주문완료 화면, 회원/비회원 주문 상세, 마이페이지·관리자 주문 목록에서 사용·적립 마일리지를 확인할 수 있으며, 관리자 주문 상세에는 환불된 마일리지도 함께 표시됩니다.", + "관리자 주문 상세의 주문상품 표에서 상품별 적립 예정액 옆에 실제 적립 여부(\"적립완료\"/\"적립예정\")를 함께 확인할 수 있습니다.", + "장바구니에 담긴 상품이 설정한 보관기간이 지나면 자동으로 정리됩니다. 환경설정의 장바구니 보관기간(일)에 따라 마지막 활동 이후 기간이 지난 항목을 매일 자동 삭제하며, 보관기간을 0(또는 비움)으로 두면 자동 삭제하지 않습니다.", + "배송정책 계산 API의 요청 참고 필드를 시스템이 지원하는 항목(배송정책 ID·국가 코드·주문 항목·그룹 합계 금액·총 수량) 중에서 선택하도록 개선했습니다. 자유 입력으로 인해 지원하지 않는 필드명이 조용히 무시되던 문제를 막고, 선택하지 않으면 모든 항목을 전송합니다.", + "배송정책 계산 API 연동을 외부 API 규격에 맞춰 세부 설정할 수 있도록 확장했습니다.", + "요청 방식을 GET 또는 POST로 선택할 수 있습니다.", + "인증이 필요한 API를 위해 Bearer 토큰 또는 커스텀 헤더(API 키 등)를 지정할 수 있습니다. 저장한 토큰은 안전하게 보관되며 화면에는 가려져 표시됩니다.", + "요청 항목별로 외부 API가 요구하는 필드명을 따로 지정(매핑)할 수 있어, 항목 이름이 다른 API에도 맞출 수 있습니다.", + "응답이 JSON일 때 배송비 값의 위치를 점(.)으로 중첩 경로(예: data.shipping.fee)까지 지정할 수 있고, 텍스트로 응답하는 API는 통화기호·콤마가 섞여 있어도 금액을 인식합니다.", + "\"테스트 호출\" 버튼으로 입력한 설정 그대로 실제 API를 한 번 호출해 요청 내용과 응답·추출된 배송비를 즉시 확인할 수 있습니다.", + "무통장입금 주문의 입금을 관리자가 직접 확인할 수 있는 \"입금확인\" 기능이 추가되었습니다 — 주문 상세 결제정보에서 입금자명과 입금액을 입력해 입금을 기록하며, 입금액이 결제예정금액과 정확히 일치할 때만 처리됩니다. 입금확인 버튼은 무통장입금이면서 입금이 아직 확인되지 않은 결제 건에 표시됩니다. \"주문을 결제완료로 처리\" 체크박스를 함께 제공해, 체크하면 입금 기록과 함께 주문을 결제완료로 변경(마일리지 적립·재고 차감·알림 발송)하고, 체크하지 않으면 입금 내역만 기록합니다. 이미 결제완료된 주문에 추가 금액(교환·반품 배송비 등)을 별도로 입금받아 기록하는 경우에 활용할 수 있습니다.", + "\"배송 완료\" 알림이 추가되었습니다 — 주문이 배송완료 상태로 바뀌면 구매자에게 택배사·운송장번호와 함께 배송완료 안내(메일·알림함)가 발송됩니다.", + "상품 등록·수정 화면의 기타정보에서 새 라벨을 바로 추가할 수 있습니다 — \"라벨 추가\" 버튼으로 이름·색상을 입력해 등록하면 곧바로 해당 상품에 연결됩니다. 같은 모달에서 기존 라벨 수정도 가능합니다.", + "추가옵션 선택지에 \"직접입력 허용\"을 켤 수 있습니다 — 켜면 구매자가 그 선택지를 골랐을 때 각인 문구처럼 원하는 내용을 직접 입력해 주문할 수 있고, 입력한 내용은 장바구니·주문서·주문 상세에 함께 표시되며 주문 시점 그대로 보존됩니다. 필수 추가옵션의 직접입력 선택지는 내용을 입력해야 담기·주문이 진행되고, 필수가 아닌 추가옵션은 비워 두어도 됩니다. 직접입력 내용이 다르면 장바구니에서 별도 항목으로 담깁니다.", + "추가옵션을 선택한 주문이 마이페이지 주문 내역과 관리자 주문 관리 목록에도 대표 옵션과 함께 추가옵션 요약(\"첫 추가옵션 외 N건\")으로 표시됩니다 — 직접입력한 내용도 함께 병기됩니다. 이전에는 목록에서 대표 옵션만 보여 추가옵션 주문 여부를 알 수 없었습니다.", + "추가옵션 그룹·선택지 등록 시 입력 검증 안내가 정확한 우리말 안내로 표시됩니다 — 선택지를 등록하지 않은 빈 그룹을 저장하면 영문 키 대신 \"각 추가옵션 그룹에는 선택지를 1개 이상 등록해주세요.\" 안내가 해당 그룹 영역 강조와 함께 표시됩니다.", + "관리자가 쿠폰 목록에서 회원을 검색·선택해 쿠폰을 즉시 발급할 수 있습니다 — 회원 이름·이메일을 입력하면 실시간으로 검색되며, 발급 한도에 도달한 회원은 자동으로 제외되고 발급 결과를 안내합니다.", + "관리자가 쿠폰 발급 내역에서 발급된 쿠폰의 사용 여부와 사용된 주문(주문번호 바로가기)을 확인할 수 있습니다.", + "관리자가 아직 사용하지 않은 발급 쿠폰을 발급취소할 수 있습니다 — 취소하면 해당 발급분이 회수되어 회원 쿠폰함에서 사라지고 발급 수량이 복원됩니다. 이미 사용했거나 만료·취소된 발급 건은 취소할 수 없습니다.", + "장바구니·주문서 작성 화면에 정가(취소선)·할인율(%)·판매가가 함께 표시되며, 보조 통화를 선택하면 정가도 같이 환산되어 보입니다.", + "회원이 마이페이지에서 결제 통화를 직접 설정할 수 있으며, 관리자도 회원 편집 화면에서 회원별 결제 통화를 지정할 수 있습니다. 로그인하면 헤더에서 임시로 고른 통화 대신 계정에 저장된 통화가 우선 적용됩니다.", + "회원 가입 시 브라우저 언어/지역에 맞는 결제 통화가 자동으로 지정됩니다 — 환경설정의 통화별 사용 언어 매핑을 따르며, 매핑이 겹치거나 없으면 기본 통화가 지정됩니다. 관리자는 환경설정에서 통화별 사용 언어를 여러 개 지정할 수 있습니다.", + "상품 또는 주문이 한 건이라도 등록된 후에는 데이터 정합성을 위해 기본 통화를 변경할 수 없습니다.", + "상품 목록·검색·인기 상품, 상품 상세의 옵션 합계 금액, 상품 상세 쿠폰, 체크아웃의 쿠폰·배송비 쿠폰 금액이 모두 선택한 통화로 환산되어 표시됩니다 — 그동안 이 영역들이 기본 통화(원)로 고정되어 보이던 문제를 바로잡았습니다. 헤더에서 통화를 바꾸면 상품 목록 가격도 즉시 함께 바뀝니다.", + "마일리지 \"통화별 사용 단위\" 설정에서 통화는 언어·통화 설정에 등록된 통화 중에서만 선택할 수 있고, 기본 통화에 대한 사용 단위가 설정되지 않으면 결제 시 마일리지를 사용할 수 없습니다.", + "주문 시 선택한 배송 메모가 내부 코드 대신 안내 문구(라벨)로 저장·표시됩니다 — 마이페이지 주문 상세와 관리자 주문 상세에서 \"경비실에 맡겨주세요\" 같은 문구가 그대로 보이며, 직접 입력한 자유 메모는 입력한 내용이 보존됩니다.", + "주문 상세의 배송 현황에서 운송장 번호를 누르면 택배사 배송조회 페이지가 새 창으로 열립니다 — 운송장 정보가 없으면 기존처럼 번호만 표시됩니다.", + "결제창으로 넘어가는 청구 금액이 결제 통화로 환산되어 전달됩니다 — 결제창에는 주문 시점 환율로 환산한 결제 통화 금액(화면에 표시된 총 결제금액과 동일한 값)이 전달되며, 달러처럼 소수점을 쓰는 통화는 결제대행사 규격에 맞춰 청구됩니다. 결제할 수 없는 통화(환율이 설정되지 않은 통화 등)로는 주문 단계에서 안내와 함께 결제가 차단됩니다. 무통장입금 안내액도 같은 기준으로 결제 통화 금액으로 안내됩니다.", + "환율/통화 설정에서 통화마다 \"사용 언어\"를 사이트가 지원하는 언어 중에서 지정할 수 있습니다 — 방문자가 보는 사이트 언어에 맞춰 표시 통화를 자동으로 정하는 데 쓰입니다. 로그인하지 않은 방문자도 현재 사이트 언어에 해당하는 통화로 가격이 표시되며(예: 영어로 보는 방문자에게는 영어로 지정된 통화), 회원 가입 시에도 같은 기준으로 결제 통화가 자동 지정됩니다. 방문자가 헤더에서 직접 고른 통화가 있으면 그 선택이 우선합니다. 처음 제공되던 통화 중 위안·유로에 지원하지 않는 언어가 들어 있어 통화 설정을 저장하면 \"설정 저장에 실패했습니다\"로 막히던 문제를 해결했습니다 — 기본값을 원은 한국어, 그 외 통화는 영어로 정리하고, 기존에 저장된 설정에서도 지원하지 않는 언어를 자동으로 정리합니다(직접 지정한 지원 언어는 그대로 유지). 지원하지 않는 언어를 선택해 저장하면 해당 입력칸이 빨갛게 강조되고 칸 아래에 \"사용 언어\" 항목명으로 사유가 표시됩니다.", + "쇼핑몰 본인인증 정책 편집 화면에서 \"인증 목적\"과 \"적용 대상\"을 바꿔 저장해도 반영되지 않던 문제를 수정했습니다 — 이제 정책 키·강제 시점·강제 위치를 제외한 모든 항목(인증 목적·적용 대상 등)을 자유롭게 변경할 수 있으며, 변경한 값은 업데이트 후에도 유지됩니다.", + "쇼핑몰 환경설정의 본인인증 정책 탭에서 출처 필터(전체/코어/모듈/플러그인)를 골라도 결과가 항상 비어 보이던 문제를 해소했습니다 — 이 탭은 쇼핑몰 정책만 보여주는 화면이므로, 동작하지 않던 출처 필터를 정리했습니다. 강제 시점 필터와 검색은 그대로 사용할 수 있으며, \"이 모듈이 등록한 본인인증 목적\" 안내는 그대로 표시됩니다.", + "데이터베이스 테이블 접두사를 기본값(`g7_`)보다 길게 설정한 환경에서 쇼핑몰 모듈 설치가 실패하던 문제를 수정했습니다 — 배송정책 관련 일부 테이블의 인덱스 이름이 데이터베이스 한도를 넘겨 설치가 중단되던 결함으로, 접두사 길이와 관계없이 설치가 정상 완료됩니다.", + "할인이 적용되지 않은 주문 항목의 금액이 주문 상세에서 표시되지 않던 문제를 수정했습니다 — 할인이 없는 항목도 소계 금액이 정상적으로 보입니다.", + "주문을 취소한 뒤에도 관리자 주문 상세·주문 목록에 \"적립 예정\" 마일리지가 계속 표시되던 문제를 수정했습니다 — 전체 취소·부분 취소된 주문에는 적립 예정 표시가 더 이상 나타나지 않습니다(실제 적립은 종전에도 발생하지 않았으며 표시만 정리되었습니다).", + "다른 언어를 쓰는 회원에게 보내는 알림(이메일·사이트 알림)이 받는 사람 본인의 언어로 발송됩니다 — 관리자가 한국어 화면에서 영어 회원의 주문 상태를 바꿔도 알림은 영어로 전달됩니다.", + "상품 일괄 적용 확인 창의 옵션 변경 요약에서 \"안전재고\"·\"사용\" 항목이 영어 화면에서도 올바른 언어로 표시됩니다 — 그동안 한국어로 고정 표시되던 문제를 바로잡았습니다.", + "공통정보 관리의 다국어 탭 저장이 정상 동작합니다 — 한국어 탭에 입력하고 저장할 때 \"한국어 필수\" 오류가 뜨거나, 영문 탭으로 옮긴 뒤 저장하면 공통정보명이 비워지던 문제를 해소했습니다.", + "주문 이력이 있는 상품을 삭제하려 하면 \"N건의 주문 이력이 있어 삭제할 수 없습니다\"라고 사유가 표시되고, 삭제 확인 창이 목록 화면과 동일하게 삭제 가능 여부를 먼저 확인해 불가 시 안내와 함께 버튼이 비활성화됩니다. 삭제에 성공한 뒤 잘못된 오류 안내가 뜨던 문제도 함께 해소했습니다.", + "장바구니 선택 삭제·전체 삭제 후 안내 문구에 실제 삭제 개수가 표시됩니다 — 그동안 \"{개수}개 상품이 삭제되었습니다\"처럼 자리표시자가 그대로 노출되던 문제를 바로잡았습니다.", + "통화 선택 메뉴가 관리자가 설정한 통화 목록을 표시하도록 수정했습니다 — 그동안 목록이 비어 통화를 고를 수 없던 문제를 바로잡았고, 환율이 설정되지 않은 통화는 메뉴에서 자동으로 숨겨집니다. 기본 화폐를 바꾸면 사용자 화면의 가격·합계·쿠폰 금액이 선택한 통화로 환산되어 표시됩니다.", + "관리자가 통화 설정을 저장할 때 일부 통화의 환율 정보가 사라지던 문제를 수정했습니다 — 빠진 통화가 기본 카탈로그에서 자동으로 보충됩니다.", + "쿠폰 다운로드 모달에서 정액 할인·최소주문금액이 선택한 통화로 환산되어 표시됩니다 — 그동안 기호만 바뀌고 금액은 환산되지 않던 문제를 바로잡았습니다(정률 할인은 통화와 무관하게 % 그대로 표시).", + "\"언어·통화\" 설정에서 기본 언어 항목을 제거하고, 사이트 언어는 일반 설정에서 일원화하여 관리합니다.", + "결제·결제 통지·부분 환불 금액이 사이트 기본 통화를 기준으로 산정되며, 선택 통화는 주문 시점 환율로 환산된 금액으로 표시·청구됩니다. 환율이 바뀌어도 기존 주문의 환불 금액은 주문 당시 환율로 계산되어 환차손이 없습니다.", + "신용카드(PG) 주문의 관리자 신규주문 알림을 결제 완료 시점에 보내도록 바꿨습니다 — 그동안 결제 전(주문서 생성) 시점에 알림을 보내면서 결제를 마치지 않은 주문도 알림이 발송되고, 알림 발송이 결제하기 응답을 지연시키던 문제가 함께 해소되었습니다. 무통장입금 주문은 기존대로 주문 접수 시점에 입금 안내가 발송됩니다.", + "상품 관리 목록에서 옵션명만 인라인으로 바꾼 뒤 일괄 적용하면 \"일괄 업데이트에 실패했습니다\"로 저장되지 않던 문제 수정 — 한국어 옵션명만 입력하고 영어·일본어를 비워 둔 옵션도 정상 저장됩니다. 또한 일괄 적용이 실패하면 어떤 항목이 왜 거부됐는지 확인 모달과 알림에 구체적인 사유가 표시됩니다.", + "상품 수정 화면에서 이미지를 추가하고 저장하면, 업로드는 됐는데도 저장 직후 새로고침하면 추가한 이미지가 모두 사라지던 문제 수정 — 이미지를 추가하고 저장할 때 그 이미지들이 상품에 정상 반영됩니다. 기존 이미지가 있는 상태에서 추가, 모두 지우고 다시 첨부, 추가 후 순서 바꾸기 모두 저장한 그대로 유지됩니다.", + "상품 관리 목록에서 기본으로 설정한 옵션의 판매가를 바꿔도 상품 판매가가 따라 바뀌지 않던 문제 수정 — 상품 등록·수정 화면과 동일하게, 기본 옵션의 판매가를 바꾸면 상품 판매가가 함께 맞춰지고 나머지 옵션 판매가도 가산액 기준으로 다시 계산됩니다.", + "상품 등록·수정 화면에서 추가옵션의 옵션명을 입력할 때 글자 입력이 버벅이던 문제 수정 — 입력이 끝난 뒤 잠깐 후에 반영되도록 바꿔 타이핑이 매끄러워집니다.", + "상품 등록·수정 화면의 추가옵션 \"필수\" 선택을 체크박스에서 켜기/끄기 토글로 바꿔, 옵션의 사용/노출 토글과 동일한 조작감으로 통일했습니다.", + "쿠폰 정액 할인값에 음수를 입력하면 정률(%) 전용 안내 문구가 잘못 표시되던 문제와, 최소주문금액을 비우고 저장하면 오류가 나던 문제를 수정했습니다 — 정액 할인값은 1원 이상으로 안내되고, 최소주문금액을 비우면 \"제한 없음\"으로 저장됩니다.", + "쿠폰 적용 대상을 \"특정 상품\" 또는 \"특정 카테고리\"로 고른 뒤 대상을 하나도 선택하지 않아도 저장되던 문제를 수정했습니다 — 대상을 1개 이상 선택해야 저장됩니다.", + "쿠폰 등록·수정 화면의 상품 검색이 입력한 검색어를 무시하고 전체 상품을 보여주던 문제를 수정했습니다.", + "\"특정 카테고리\" 쿠폰이 해당 카테고리에 속하지 않은 상품에도 노출·적용되던 문제를 수정했습니다 — 쿠폰의 카테고리와 상품의 카테고리가 일치할 때만 적용됩니다.", + "쿠폰 목록 검색에서 검색 필드(쿠폰명/설명/등록자)를 골라도 항상 전체가 검색되던 문제와, 상세 필터 선택이 주소(URL)로 복원될 때 상세 필터 영역이 펼쳐지지 않아 선택이 보이지 않던 문제를 수정했습니다.", + "상품 상세의 쿠폰 다운로드 모달을 무한 스크롤로 개선했습니다 — 페이지 전환 버튼 없이 목록을 아래로 내리면 다음 쿠폰을 한 묶음씩 이어서 불러오고(불러오는 동안 \"불러오는 중\" 표시), 전체를 다 불러오면 멈춥니다.", + "상품 등록·수정 화면의 \"공통정보\" 선택 목록에 비활성 처리한 공통정보까지 나타나던 문제 수정 — 활성 상태인 공통정보만 선택 대상으로 표시됩니다. 상품정보제공고시 템플릿과 동일하게 동작합니다.", + "신규 주문 접수 시 관리자에게 발송되는 알림 메일에서 결제금액이 0원으로 표시되던 문제 수정 — 결제 예정 금액이 정확히 표시됩니다.", + "관리자 주문 상세의 결제정보에서 \"결제요청일시\"가 비어 있고, 무통장입금의 입금 계좌(은행·계좌번호·예금주) 요약이 표시되지 않던 문제 수정 — 주문 접수 시 결제 요청 시각과 구매자 정보가 기록되며, 무통장입금 주문에 입금 계좌 요약이 표시됩니다.", + "신용카드(PG) 결제가 완료된 뒤에도 주문 상세·마이페이지의 개별 상품 \"주문상태\"가 \"주문대기\"로 남던 문제 수정 — 결제가 완료되면 주문 상품 상태도 함께 결제완료로 표시됩니다. 결제 금액 전액을 마일리지로 충당한 주문도 동일하게 반영됩니다.", + "관리자가 주문 상세에서 주문 상태를 변경할 때 주문 전체 상태만 바뀌고 개별 상품 상태는 이전 상태로 남던 문제 수정 — 상품 상태도 함께 변경됩니다. 이미 취소된 상품은 변경 대상에서 제외되어 그대로 유지됩니다.", + "결제 실패·결제창 닫힘으로 주문이 취소될 때 개별 상품 상태가 \"주문대기\"로 남던 문제 수정 — 주문이 취소되면 상품 상태도 함께 취소로 정리되며, 이미 부분취소된 상품은 그대로 보존됩니다. 그동안 상품 상태가 주문대기에 갇혀 구매확정·리뷰 작성이 막히던 연쇄 문제도 함께 해소됩니다.", + "신용카드 결제가 정상 완료됐는데도 주문 상세에서 주문은 \"결제완료\"이면서 개별 상품만 \"주문취소\"로 표시되던 문제 수정 — 결제 승인과 결제창 닫힘 처리가 겹칠 때 결제가 이미 완료된 주문은 취소 처리하지 않도록 바로잡아, 결제완료 주문의 상품이 임의로 취소 상태가 되지 않습니다.", + "관리자가 주문 상태를 결제완료·배송중·배송완료·구매확정으로 바꿔도(단건/목록 일괄/운송장 일괄 포함) 구매자에게 해당 알림이 발송되지 않던 문제 수정 — 모든 상태변경 경로에서 결제완료·배송중·배송완료·구매확정 알림이 누락 없이 발송됩니다. 이미 같은 상태인 주문을 다시 저장할 때는 중복 발송되지 않습니다.", + "결제 완료 시 발송되는 알림의 제목·본문이 \"주문 확인\"으로 표시되어 결제 완료 시점과 의미가 어긋나던 문구를 \"결제 완료\"로 명확히 정정했습니다.", + "본인인증 정책(결제 시 확인·무통장 입금확인·결제 승인·결제 취소)을 활성화해도 실제 액션 시 본인인증이 요구되지 않던 문제 수정 — 4종 정책 모두 실제 결제·취소·입금확인·주문 진입 시점에 정상 동작합니다.", + "상품 \"구매 대상 제한\" 설정 화면에서 제한으로 전환할 때 이전 역할이 빈 칩으로 남던 문제와, 역할이 많을 때 일부 역할(관리자 등)이 선택 목록에서 누락되던 문제 수정", + "회원 주문 상세에서 \"배송 메모\"·\"배송 현황\" 영역이 표시되지 않던 문제 수정 — 회원/비회원 모두 동일한 배송 정보가 표시됩니다.", + "상품 상세 페이지의 검색엔진용 구조화 데이터(Schema.org Product/Offer)에서 재고 상태가 실제 판매 상태와 무관하게 항상 \"품절(OutOfStock)\"로 출력되던 결함 수정. 판매 중인 상품은 \"재고 있음(InStock)\", 판매 중지·예정 상품은 \"품절\"로 정확히 출력됩니다.", + "레이아웃 편집기 라우트 트리에서 상품정보제공고시 화면이 잘못된 다국어 키 참조로 라벨이 비어 보이던 결함 수정. 정확한 화면명(\"상품정보제공고시 관리\")이 표시됩니다.", + "레이아웃 편집기 라우트 트리에서 상품 등록 단축 경로(`/admin/ecommerce/product-create`) 가 다국어 라벨 없이 경로 그대로 표시되던 문제 수정. \"상품 등록\" 라벨에 \"리다이렉트\" 배지를 함께 표시합니다.", + "쇼핑몰 환경설정의 결제수단·통화·배송 국가 목록 라벨이 일본어 등 비한국어 로케일에서도 한국어로 표시되던 결함 수정. 활성 언어팩의 번역 라벨이 정확히 노출됩니다.", + "주문 취소 시 재고가 이중으로 복구되던 결함 수정. 전체취소·부분취소 모두 취소한 수량만큼만 정확히 한 번 복구됩니다(이전에는 전체취소 시 최대 두 배가 복구되어 오버셀 위험이 있었습니다).", + "환경설정의 \"취소 시 재고 복구\" 끄기 설정이 무시되어, 꺼 두어도 취소 시 재고가 복구되던 문제 수정. 이제 설정대로 켜면 복구, 끄면 복구하지 않습니다.", + "무통장입금 주문이 입금 기한이 지나도 자동으로 취소되지 않던 문제 수정. 가상계좌와 무통장입금 결제대기 주문 모두 설정한 자동취소 기한이 지나면 정상적으로 자동 취소됩니다.", + "무통장입금 주문의 결제 정보에서 입금 은행·계좌 요약이 비어 보이던 문제 수정. 주문 상세에서 입금 은행명과 계좌번호가 정상 표시됩니다.", + "취소했던 주문을 다시 판매 상태(결제완료 등)로 되돌릴 때, 취소로 복구되었던 재고가 다시 차감됩니다. 되돌리려는 시점에 재고가 부족하면 상태 변경이 차단되어 재고가 음수로 빠지거나 오버셀이 발생하지 않습니다. 단건 변경·목록 일괄 변경과 더불어, 주문 상세에서 상품(옵션) 단위로 상태를 되돌리는 경우에도 동일하게 재고가 재차감됩니다.", + "주문 상세에서 상품(옵션) 단위로 상태를 \"주문취소\"로 바꿀 때, 결제(PG) 취소를 함께 하지 않는 경우에는 재고가 복구되지 않던 문제 수정. 이제 결제 취소 동반 여부와 관계없이 취소 시 재고가 설정대로 복구됩니다. 결제를 함께 취소하는 경우에도 재고가 이중으로 복구되지 않고 정확히 한 번만 복구됩니다.", + "단일 상품 주문에서 취소 수량을 줄여 부분취소하려 해도 항상 전체취소로 처리되던 문제 수정. 취소 수량을 상품 수량보다 적게 지정하면 그 수량만 부분취소되고, 모든 상품을 전량 취소할 때만 전체취소로 처리됩니다.", + "쇼핑몰 환경설정의 \"취소 시 재고 복구\" 토글을 켜서 저장하면 \"참/거짓 값이어야 합니다\" 오류로 저장이 거부되던 문제 수정. 이제 토글을 켜고 끄는 설정이 정상적으로 저장됩니다.", + "주문서 작성 시 보유한 마일리지보다 많은 금액을 사용하려 하면, 조용히 보유 잔액으로 줄여 결제하지 않고 \"보유 마일리지를 초과하여 사용할 수 없습니다\"라고 명확히 안내하도록 변경. 의도와 다른 금액으로 결제되던 혼란을 제거했습니다.", + "판매중지·품절·출시예정·전시중지 상태의 상품이 장바구니에 담기거나 결제까지 진행되던 문제 수정. 이제 담기 단계에서 차단되며, 이미 담겨 있던 상품이 판매중지로 바뀌면 합계 금액에서 자동으로 제외되고 구매 불가로 표시됩니다.", + "상품에 설정한 최소·최대 구매 수량이 실제 구매 과정(옵션 상품 포함)에 적용되지 않던 문제 수정. 이제 같은 상품의 옵션 수량을 합산한 총수량 기준으로 최소 미만·최대 초과 구매가 차단됩니다.", + "상품 등록·수정에서 \"구매 대상 제한\"을 선택하고도 허용 역할을 하나도 선택하지 않은 채 저장되던 문제 수정. 이제 제한을 선택하면 허용 역할을 최소 1개 이상 지정해야 저장됩니다.", + "상품 상세에서 \"바로 구매\"를 하면 해당 상품이 장바구니에도 함께 담기던 문제 수정. 이제 바로 구매는 장바구니를 거치지 않으며, 장바구니에 같은 상품이 들어 있어도 그 수량과 합산되지 않고 지금 선택한 수량만으로 주문됩니다(구매 수량 제한도 이번 선택 수량 기준으로 적용). 바로 구매 후 장바구니에는 기존 항목만 그대로 유지됩니다.", + "장바구니 담기·바로 구매가 차단될 때 \"담을 수 없습니다\" 같은 일반 안내만 나오던 문제 수정. 이제 어떤 상품이 왜(판매 중지·품절·구매 권한 없음·최소/최대 구매 수량 초과) 막혔는지 상품명과 함께 구체적으로 안내합니다.", + "쇼핑몰 상품 목록에서 상위 카테고리를 선택하면 그 하위 카테고리에만 속한 상품이 함께 표시되도록 개선. 이전에는 상위 카테고리 버튼을 눌러도 직접 지정된 상품만 나오고 하위 카테고리 상품은 누락되었으며, 카테고리 버튼이 목록을 실제로 필터링하지 않던 문제도 함께 수정했습니다. 선택한 카테고리는 버튼에 표시되어 현재 어떤 분류를 보고 있는지 알 수 있습니다.", + "관리자 상품 목록에서 검색어로 조회하면 결과는 나오는데 \"총 N개\"가 0으로 잘못 표시되던 문제 수정. 이제 검색 결과 수가 실제 목록과 일치하며, 검색어와 판매 상태 등 다른 필터를 함께 적용해도 정확히 집계됩니다. 관리자 쿠폰 목록의 검색 총 개수 오표시도 동일하게 수정했습니다.", + "관리자 상품 목록의 \"카테고리 미등록\"·\"브랜드 미등록\" 필터를 켜면 오류가 발생하며 영문 키가 그대로 노출되던 문제 수정. 이제 해당 필터가 정상 동작하여 분류가 지정되지 않은 상품만 정확히 조회됩니다.", + "관리자 상품·주문 목록에서 검색어를 빠르게 입력한 직후 곧바로 Enter 를 누르면 방금 입력한 검색어가 누락된 채 조회되던 문제 수정. 검색 입력 시 화면 버벅임도 함께 줄였습니다.", + "무통장입금 주문을 취소할 때 결제(PG) 취소 처리가 실패하며 취소 자체가 막히던 문제 수정. 무통장 등 PG를 거치지 않는 결제수단은 취소 시 결제대행 환불 단계를 건너뛰며, 환불금은 운영자가 직접 송금하도록 \"환불 승인(대기)\" 상태로 표시됩니다.", + "입금 전(미입금)이거나 실제 결제금액이 0원인 주문에서 일부 상품을 부분취소하려 하면 항상 \"취소할 수 없습니다\"로 막히던 문제 수정. 실제로 낸 돈이 없는 주문은 환불·추가청구 대상이 아니므로 그대로 부분취소됩니다.", + "여러 상품이 담긴 주문에서 일부 상품만 선택해 취소해도 주문 전체가 취소되던 문제 수정. 선택한 상품만 정확히 취소되고, 남은 상품은 그대로 유지됩니다. 모든 상품을 전량 취소할 때만 주문 전체가 취소됩니다.", + "일부 상품만 취소한 주문도 남은 상품을 이어서 취소할 수 있습니다. 남은 상품을 모두 취소하면 주문 전체가 취소 처리됩니다.", + "일부 상품만 취소한 주문이 그 주문의 실제 진행 단계(결제완료·상품준비중·배송중 등)에서 사라지지 않고 그대로 추적되도록 개선했습니다. 일부가 취소된 주문에는 주문내역·주문 상세에 \"일부취소\" 표시가 함께 붙어, 남은 상품은 정상 배송 흐름을 따르면서 일부 취소 사실도 한눈에 확인할 수 있습니다. (관리자 주문 목록·상세에도 동일하게 표시됩니다.)", + "취소 차단 안내 문구가 \"쿠폰 할인 조건\" 으로 단정하던 표현을 실제 사유(적용 중인 할인 조건 변경으로 결제금액 증가)에 맞게 일반화했습니다.", + "마이페이지 주문내역의 \"상품준비중\" 건수에는 배송준비완료 주문까지 포함되는데, 그 건수를 눌러 필터링하면 배송준비완료 주문이 빠져 표시 건수와 목록 수가 어긋나던 문제 수정. 이제 건수와 목록이 일치합니다.", + "무통장입금 등 결제대행을 거치지 않는 주문의 취소 화면에서 의미 없는 \"결제(PG) 함께 취소\" 선택이 노출되던 문제 수정. 해당 결제수단에서는 자동으로 숨겨집니다. 또한 \"환불 우선순위\"(PG↔마일리지 배분 순서)는 사용한 마일리지가 없는 주문에서 선택 의미가 없으므로, 항목은 보이되 선택할 수 없도록(비활성) 처리하고 사유를 함께 안내합니다.", + "주문 취소 시각이 전체취소에만 기록되고 부분취소에는 남지 않던 문제 수정. 전체·부분취소 모두 취소 시각이 정확히 기록됩니다.", + "관리자 배송정책 목록에서 데이터가 없을 때 빈 표 머리글과 \"검색 결과가 없습니다\" 안내가 동시에 어색하게 겹쳐 보이던 문제 수정. 쿠폰·주문·상품 목록과 동일하게 표 영역에 빈 상태 안내만 깔끔하게 표시되도록 정리했습니다.", + "무통장입금·가상계좌 입금 대기 주문의 자동취소 기한이 환경설정의 자동취소 기한(일)대로 적용됩니다. 이전에는 이 설정이 실제 동작에 반영되지 않았으며, 결제수단별로 나뉘어 있던 입금기한 설정을 하나의 자동취소 기한으로 통일했습니다(기존에 설정해 둔 값은 그대로 보존됩니다).", + "리뷰 작성 화면에 리뷰 이미지 최대 개수·최대 용량 설정이 실제로 반영됩니다. 이전에는 설정과 무관하게 항상 기본값(5장·10MB)이 적용되었으며, 최대 개수를 0으로 두면 이미지 첨부 영역이 숨겨집니다. 용량 초과 시 안내 메시지에도 설정한 용량이 표시됩니다.", + "리뷰 작성 가능 기간(구매확정 후 일수) 설정이 리뷰 작성 버튼 노출과 실제 작성 가능 여부에 동일하게 적용됩니다. 이전에는 두 판정이 미세하게 어긋나 버튼은 보이는데 작성이 막히거나 그 반대가 될 수 있었고, 기간을 0(무제한)으로 두면 버튼이 사라지던 문제도 해소되었습니다.", + "배송정책 목록이 빈 화면일 때 안내 문구가 한 번만 표시되도록 정리했습니다. 상품 등록·수정 폼의 배송정책 선택에는 사용 중인(활성) 정책만 표시되며, 기존 상품에 적용된 정책이 비활성화된 경우에는 그 정책을 그대로 유지하면서 \"비활성화된 배송정책 적용 중\" 안내를 함께 보여 줍니다.", + "배송정책 계산 API 주소를 입력하지 않은 채 저장하면 조용히 기본 배송비로 처리되던 문제를 막아, 계산 API 정책을 선택하면 API 주소 입력을 요구합니다. 또한 구간별 배송비·계산 API 설정에서 음수나 잘못된 값 입력 시 오류 메시지가 내부 코드 경로로 표시되던 문제를 수정해, 이해하기 쉬운 안내 문구로 표시됩니다.", + "상품정보제공고시(및 공통정보) 관리 목록의 무한 스크롤을 정비했습니다. 목록 상단의 개수가 전체 등록 건수를 정확히 표시하고(이전에는 처음 불러온 만큼만 표시), 스크롤하면 다음 항목을 한 묶음씩 불러오다 전체를 다 불러오면 멈춥니다(이전에는 더 불러올 항목이 없어도 추가 요청을 반복). 다음 묶음을 불러오는 동안 \"불러오는 중\" 표시가 화면에 보이도록 했습니다. 같은 방식의 배송정책 \"추가 배송비 템플릿\" 선택 목록과 쿠폰 \"상품 검색\" 목록에도 동일하게 적용했습니다. 목록에서 각 항목의 사용 여부를 바로 켜고 끌 수 있는 토글을 추가했고, 상품 등록·수정 폼의 고시 선택에는 사용 중인 항목만 표시됩니다.", + "상품 옵션을 장바구니에 담거나 수량·옵션을 바꿀 때 품절·재고 부족이면 오류 화면 대신 \"재고가 부족합니다 (요청 N개, 가용 M개)\"처럼 명확한 안내가 표시됩니다. 이미 삭제됐거나 다른 사용자의 장바구니 항목을 조작하려 할 때도 알 수 없는 오류 대신 적절한 안내가 표시됩니다.", + "주문 상태를 진행 순서에 어긋나게(예: 구매확정 → 결제완료) 변경하던 문제 수정 — 관리자 주문 관리의 단건·일괄·상품옵션별 상태 변경 모두에서 허용되지 않는 상태 전이를 차단합니다. 정상 진행 방향(중간 단계 건너뛰기 포함)과 운영상 필요한 일부 되돌림(배송중→상품준비중 등), 취소했던 주문을 다시 판매 상태로 되살리는 복원은 그대로 허용됩니다. 차단될 때에는 \"구매확정 상태에서 결제완료 상태로는 변경할 수 없습니다\"처럼 어떤 전이가 왜 막혔는지 알 수 있는 안내가 표시됩니다(이전에는 주문 일괄·상품옵션 일괄 변경에서 \"Request failed with status code 422\"나 일반 실패 문구만 보였습니다).", + "상품 쿠폰의 \"최소 주문금액\" 조건이 주문 전체 합계가 아니라 그 쿠폰을 적용한 상품 금액 기준으로 검증되도록 수정. 다른 상품을 함께 담아 전체 금액만 채워 최소 주문금액 조건을 우회하던 문제를 막았습니다(주문·배송비 쿠폰은 종전처럼 전체 합계 기준).", + "쿠폰의 \"1인 사용 횟수 제한\"이 주문 단계에서 실제로 적용되도록 수정. 이전에는 다운로드 시점에만 확인되어, 한 주문에 같은 쿠폰을 여러 상품에 중복 적용하거나 이미 사용한 쿠폰을 다시 사용할 수 있었습니다. 이제 과거 사용 횟수와 한 주문 내 중복 적용을 합산해 한도를 초과하면 차단합니다.", + "\"다른 쿠폰과 함께 사용 불가\"로 설정한 쿠폰이 서로 다른 종류의 쿠폰(상품·주문·배송비)과 동시에 적용되던 문제 수정. 이제 어떤 종류의 쿠폰과도 함께 적용되지 않도록 차단합니다.", + "만료되었거나 존재하지 않는 주문서(임시 주문)에 접근할 때 서버 오류(500)로 응답하던 문제 수정. 이제 한국어·영어 환경 모두에서 \"찾을 수 없음\"(404)으로 정확히 응답하며, 주문서 작성 단계에서 적용 불가한 쿠폰(최소금액 미달·사용 한도 초과·중복 불가)이 있으면 해당 쿠폰을 할인에서 제외하고 사유를 안내합니다. 주문 확정 시 적용 불가 쿠폰이 남아 있으면 결제가 차단됩니다.", + "무통장입금 주문에서 1회 사용 제한 쿠폰이 입금 처리 전까지 다시 사용되던 문제 수정. 이제 주문 접수 즉시 쿠폰이 사용 처리되며, 미입금으로 자동취소되거나 주문을 취소하면(부분취소로 적용 조건이 깨지는 경우 포함) 해당 쿠폰이 다시 사용 가능하도록 복원됩니다. (#57 @ChoDongHyeon 님께서 제보해주셨습니다.)", + "상품 등록·수정의 옵션에서 기본 옵션 판매가를 바꾸면 상단 판매정보의 판매가가 함께 갱신되도록 수정 — 기본 옵션 라디오를 다른 옵션으로 바꿀 때도 상단 판매가가 새 기본 옵션 가격으로 맞춰지며, 비기본 옵션은 기존 가산액을 유지한 채 재계산됩니다.", + "추가옵션을 \"사용\"으로 켠 뒤 항목을 추가해도 설정이 \"미사용\"으로 풀리지 않도록 수정 — 추가옵션 항목 유무로 표시 상태가 결정되며, \"미사용\"으로 끌 때 입력한 항목이 있으면 확인 후 비웁니다.", + "상품 이미지를 상한(20장)보다 많이 선택하면 조용히 잘리지 않고 안내가 표시되도록 수정했고, 대표 이미지 안내 문구를 실제 동작(★ 버튼으로 지정)에 맞게 정정했습니다. 서버에서도 상품당 이미지 개수 상한을 검증합니다.", + "SEO 제목·설명을 직접 입력해 저장하면 상품명·설명으로 덮어써지던 문제 수정 — 동기화 사용 여부가 저장되어, 끄면 입력한 값이 그대로 유지되고 켜면 상품명·설명으로 자동 채워집니다. 동기화를 끈 뒤 수정 화면을 다시 열면 끈 상태가 그대로 복원됩니다.", + "판매가·재고 일괄변경에서 상품/옵션 헤더로 전체선택했을 때 \"변경할 항목 없음\"으로 표시되던 문제 수정 — 전체선택에서도 대상이 정확히 집계되어 변경이 적용됩니다. 변경 완료 안내 문구에 실제 변경 건수가 표시됩니다.", + "상품 라벨 적용 기간의 날짜를 직접 입력할 때 화면 오류(\"컴포넌트 로드 실패\")가 발생하던 문제 수정 — 라벨 기간 날짜, 쇼핑몰 연동 설정, 양식 저장 모달 입력이 모두 정상 동작합니다.", + "추가옵션이 등록된 상품을 수정 화면에서 열면 추가옵션이 표시되지 않고 \"미사용\"으로 보여 저장 시 사라질 수 있던 문제 수정 — 저장된 추가옵션이 그대로 불러와집니다.", + "상품 등록·수정의 추가옵션 \"미사용\" 확인 창, 공지 양식 저장 창이 열리지 않던 문제 수정 — 확인 창이 정상적으로 뜨고, 공지 양식 저장 창에서 이름을 입력해 저장할 수 있습니다.", + "상품 관리 일괄변경 확인 창에서 판매·전시 상태가 \"on_sale\", \"visible\" 같은 내부 코드값으로 표시되던 문제 수정 — 드롭다운에서 고른 그대로 \"판매중\", \"전시중\" 등 한글 라벨로 표시됩니다.", + "상품 관리 재고 일괄변경 확인 창의 옵션별 변경 내용이 \"재고: undefined undefined\"로 표시되던 문제 수정 — 입력한 \"+10개\" 같은 변경 값이 그대로 표시됩니다. 가격 일괄변경의 옵션별 표시도 동일하게 정정됩니다.", + "쇼핑몰 환경설정에서 상품 목록·카테고리·상품 상세의 \"SEO 제공 페이지\"를 꺼도 검색엔진 사이트맵(sitemap.xml)에 해당 주소가 그대로 남던 문제 수정 — 끈 페이지 유형의 주소는 사이트맵에서 제외됩니다." + ] + }, + { + "name": "Security", + "items": [ + "결제 거래번호 DB 레벨 멱등성 보장 — PG 결제 콜백이 동일한 거래번호로 중복 수신되어도 DB 가 자동 차단하여 중복 결제완료/포인트 적립 등의 사고를 원천 차단" + ] + } + ] + }, + { + "version": "1.0.0-beta.4", + "date": "2026-05-12", + "categories": [ + { + "name": "Added", + "items": [ + "이커머스 환경설정의 본인인증 정책 탭에 권한 기반 버튼 비활성화 적용 — 수정 권한이 없는 운영자는 \"정책 추가\" / \"수정\" / \"삭제\" / \"활성 토글\" 버튼이 자동 비활성화" + ] + }, + { + "name": "Fixed", + "items": [ + "위시리스트 활동 로그의 \"액션\" 열에 `wishlist.remove` 가 번역되지 않은 키 그대로 노출되던 문제 수정" + ] + } + ] + }, + { + "version": "1.0.0-beta.3", + "date": "2026-05-11", + "categories": [ + { + "name": "Added", + "items": [ + "상품 상세 페이지의 OG 메타태그·Product 구조화 데이터를 이커머스 모듈이 직접 선언하도록 SEO declaration 구현 — 썸네일 가로/세로 크기·가격·통화·재고 상태가 og:product:* 와 Schema.org Product/Offer/AggregateRating 스키마로 자동 출력됨", + "결제 취소·승인·무통장 입금 확인·재고 차감/복원·배송지 관리(CRUD + 기본 설정)·체크아웃 응답 변환 지점에 본인인증(IDV) 정책 등 외부 확장을 붙일 수 있는 확장점 제공", + "IDV 정책 3종(결제 취소·승인·무통장 입금 확인) 을 코어 본인인증 정책에 자동 동기화. 운영자가 관리자 UI 에서 수정한 필드(활성/유예/프로바이더/실패 모드) 는 업데이트 재설치 시 보존 — 모두 기본 비활성", + "결제 직전 성인/본인 확인 목적(`checkout_verification`) 등록 — 별도 본인인증 프로바이더 설치 시 결제 단계 전 검증 가능", + "결제 진행 직전 본인인증(`checkout_verification`) 발생 시 결제 도메인 전용 메일 문구가 발송되도록 모듈에서 메시지 템플릿을 직접 제공 (이전에는 일반 fallback 문구가 발송됨)", + "이커머스 환경설정에 \"본인인증 정책\" 탭 추가 — 결제 취소·승인 등 이커머스 컨텍스트 정책을 코어 정책 관리 화면과 동일한 UI/UX (검색·필터(강제 시점/출처)·페이지네이션·정책 추가/편집 모달 직접 호출·이 모듈이 등록한 본인인증 목적(checkout_verification) 메타 카드 상단 노출·데스크탑 테이블/모바일 카드 자동 분기 + enabled 인라인 토글) 로 한 곳에서 관리. 정책 목록 \"인증 목적\" 컬럼이 코드명 대신 사람이 읽을 수 있는 라벨로 표시", + "결제 직전 본인 확인 정책 신설 (`checkout_verification` 목적) — 운영자가 활성화하면 결제 진행 전 자동 본인인증 진행", + "이커머스 모듈 자체 권한 신설: `sirsoft-ecommerce.identity.policies.{read,update}` (정책 관리 최소 권한)", + "이커머스 알림 발송 이력 / 본인인증 이력 샘플 시더 추가 — `module:seed sirsoft-ecommerce --sample` 으로 결제·문의·주문 등 이커머스 영역 더미 이력만 독립 생성 (코어/타 모듈 영역 침범 없음)", + "이커머스 알림 정의 7종(주문 확인·배송 시작·구매 확정·주문 취소·신규 주문 관리자·상품 문의 접수·문의 답변)을 모듈 manifest 에서 직접 선언하도록 통일 — 활성화/업데이트 시 운영자 편집값을 보존하면서 자동 등록·정리되며, 권한·메뉴·본인인증과 동일한 declarative 패턴으로 일관화", + "활동 로그의 이커머스 액션 라벨(주문·상품·카테고리·쿠폰·배송 정책 등)을 이커머스 모듈 다국어 파일에서 관리하도록 정리 — 모듈 라벨이 자기 영역에서 자기설명되며, 일본어 등 추가 언어팩이 키 누락 없이 동기화" + ] + }, + { + "name": "Changed", + "items": [ + "코어 최소 요구 버전을 7.0.0-beta.4 로 상향 — 본인인증 정책 인프라 의존", + "결제 본인 확인 인증 목적 라벨/설명을 모듈 다국어 파일에서 관리하도록 정리 (운영자/번역자가 lang 파일만 편집해도 라벨 변경 가능)", + "클레임 사유·배송 유형·배송사의 다국어 라벨이 활성 언어팩 추가 시 자동으로 새 언어 키를 포함하도록 정합화 — 운영자가 한국어 라벨만 직접 수정한 row 도 한국어는 보존하면서 신규 언어(예: 일본어) 라벨은 자동 동기화", + "배송사 다국어 라벨이 활성 언어팩의 데이터를 자동으로 머지하도록 시더 정합화 (다른 entity 시더와 일관)" + ] + }, + { + "name": "Fixed", + "items": [ + "모듈 시더 실행 시 배송 유형 시더가 자동 호출되지 않던 회귀 수정 — 언어팩 활성/비활성 토글 시 즉시 다국어 라벨 반영", + "일부 주문에서 옵션 정보 직렬화 시 500 에러가 발생하던 문제 수정", + "상품 옵션 삭제 검증 시 런타임 에러가 발생하던 문제 수정", + "설치 직후 또는 활성 모듈 디렉토리 부재 시 환경설정 기본값이 비어있던 문제 수정 — 활성 디렉토리 부재 시 번들 디렉토리를 fallback 으로 사용", + "알림 템플릿 편집 모달의 입력 필드에서 글자를 입력할 때마다 화면이 심하게 버벅이던 문제 수정", + "이커머스 환경설정 알림 탭의 채널 라벨이 식별자(mail / database)로 노출되던 문제 수정 — 활성 언어 기준의 사람이 읽을 수 있는 라벨로 표시" + ] + } + ] + }, + { + "version": "1.0.0-beta.2", + "date": "2026-04-20", + "categories": [ + { + "name": "Added", + "items": [ + "**알림 시스템 강화**", + "7종 알림(주문확인/배송시작/구매확정/주문취소/신규주문/문의접수/문의답변) 실제 발송 구현", + "알림 목록에 채널별 수신자 뱃지 표시", + "알림 편집 모달에 수신자 설정 섹션 추가 — 타입 선택 및 규칙 관리", + "알림 클릭 시 이동 URL 기본값 설정", + "알림 정의 일괄 초기화 기능 — 확인 모달 + 진행 스피너 UX 포함", + "이커머스 알림 정의를 코어 리셋 로직에 자동 기여하여 기본값 복원 가능", + "알림 탭은 코어 알림 권한 기준으로 편집 가능 여부를 판단하도록 개선 (모듈 설정 권한과 분리)", + "**SEO 환경설정 강화**", + "페이지 타입별 SEO 변수 메타데이터 선언 (상품/카테고리/검색/쇼핑몰 메인)", + "쇼핑몰 메인 페이지 메타태그 설정 추가", + "캐시 삭제 확인 모달 및 캐시 용량 동적 표시", + "**배송유형 관리 기능**", + "환경설정 > 배송설정에서 배송유형 추가/수정/삭제/활성화 가능", + "기본 11종(국내 7종 + 해외 2종 + 기타 2종) 시딩", + "배송유형/배송사 테이블 드래그앤드롭 순서 변경", + "배송방법 \"직접입력\" 선택 시 다국어 배송방법명 입력 지원", + "클레임 사유(ClaimReason) 사용자 수정 보존 지원", + "주소 중복 등록 시 전용 도메인 예외 처리" + ] + }, + { + "name": "Changed", + "items": [ + "코어 최소 요구 버전을 7.0.0-beta.2 로 상향", + "알림 정의 시더를 인라인 방식으로 전환 — 사용자 수정 자동 보존 패턴 적용", + "레거시 메일 템플릿 테이블을 통합 알림 시스템으로 이관", + "알림 수신자를 채널별 독립 관리 방식으로 변경", + "이전 업그레이드 스텝에 클래스 부재 시 안전 스킵 처리 추가", + "상품 설명/공통정보 에디터를 확장 포인트(extension_point)로 변환", + "SEO 캐시 무효화를 코어 공통 캐시 인터페이스로 전환", + "상품정보제공고시, 공통정보, 알림 템플릿 편집의 다국어 탭을 동적 생성으로 전환", + "배송유형을 Enum에서 DB 테이블로 변경하여 관리자 화면에서 동적 관리 가능하도록 변경", + "주문 필터, 배송정책 폼의 하드코딩 옵션을 DB 기반 동적 렌더링으로 변경", + "주문/배송정책에서 직접입력 배송방법명 해석 지원 (스냅샷 기반)" + ] + }, + { + "name": "Removed", + "items": [ + "레거시 메일 템플릿 시더 및 관련 다국어 키 제거", + "ShippingTypeEnum, ShippingMethodEnum 제거 — DB 관리 방식으로 이관", + "환경설정의 도서산간 배송비 기본설정 제거 — 배송정책별 설정으로 완전 이관" + ] + }, + { + "name": "Fixed", + "items": [ + "마이페이지 배송지 관리에서 수정 버튼에 권한 체크가 누락되어 있던 문제 수정", + "사용자 화면에서 상품 설명 HTML이 텍스트로 노출되던 문제 수정", + "상품 설명/공통정보 에디터에 HTML 모드 전환 옵션 누락 수정", + "공통정보 패널 폼 초기화 및 모달 전환 관련 버그 수정", + "주문목록 검색필터 배송방법 선택 시 검증 에러 수정", + "환경설정/상품옵션 테이블의 Select 드롭다운이 가려지는 문제 수정", + "재설치·재시드 시 주문/상품 시퀀스 counter 가 0으로 리셋되어 주문번호 중복 위험이 있던 문제 수정 — 기존 counter 보존", + "재설치·재시드 시 관리자가 수정·추가한 택배사 정보가 기본 목록으로 초기화되던 문제 수정 — 사용자 수정분 자동 보존" + ] + } + ] + }, + { + "version": "1.0.0-beta.1", + "date": "2026-04-01", + "categories": [ + { + "name": "Changed", + "items": [ + "오픈 베타 릴리즈" + ] + } + ] + }, + { + "version": "0.20.0", + "date": "2026-03-31", + "categories": [ + { + "name": "Added", + "items": [ + "기본 클레임 사유(환불/취소) 자동 등록 — 모듈 설치 시 7건 기본 사유 시딩 (고객 귀책 4건 + 판매자 귀책 3건)", + "`ClaimReasonSeeder` 시더 추가 (`getSeeders()` 및 `DatabaseSeeder` 등록)", + "업그레이드 스텝 `Upgrade_0_20_0` — 기존 설치 환경에 기본 클레임 사유 삽입" + ] + }, + { + "name": "Fixed", + "items": [ + "장바구니 API 서버 에러 수정 — 삭제된 배송정책을 참조하는 상품의 배송비 계산 시 null TypeError 방지, 배송비 0원 처리 (OrderCalculationService)", + "사용자 주문 취소 완료 후 모달이 닫히지 않는 문제 수정 — `showCancelModal: false` 상태값 방식을 `G7Core.modal.close('modal_cancel_order')` 호출 방식으로 교체 (userCancelOrderHandlers)", + "사용자 주문 취소 모달 취소 사유 selectbox 다크모드 미적용 수정 — 다크모드 색상 클래스 추가 (_modal_cancel.json)", + "사용자 주문 취소 모달 취소 사유 selectbox 하단 여백 누락 수정 — `mb-4` 추가 (_modal_cancel.json)" + ] + } + ] + }, + { + "version": "0.19.6", + "date": "2026-03-30", + "categories": [ + { + "name": "Fixed", + "items": [ + "상품옵션/고시정보 다국어 입력 stale closure 수정 — productOptionHandlers/productNoticeHandlers에서 디바운스 병합 시 이전 키 변경 유실 방지 (`_partial_product_options.json`, `_partial_product_notices.json`)", + "주문상세 배송방법 `$t:` 번역 토큰 원문 노출 수정 — 배송방법 표시 바인딩에 `raw:` 마커 적용 (_partial_order_info.json)", + "FileUploader onRemove 시 form 데이터 동기화 누락 수정 — remove 핸들러에 setState 추가하여 폼 데이터에서 삭제된 파일 즉시 반영 (_partial_image_upload.json)" + ] + } + ] + }, + { + "version": "0.19.5", + "date": "2026-03-30", + "categories": [ + { + "name": "Fixed", + "items": [ + "주문 일괄 변경 활동 로그 `:count` 플레이스홀더 미치환 수정 — bulk 핸들러 4개(bulkUpdate, bulkStatusUpdate, bulkShippingUpdate, bulkOptionStatusChange)의 `description_params`에 `count` 키 추가", + "주문 상태 변경 활동 로그에서 enum 값(payment_complete, shipping 등)이 번역되지 않고 원시값 노출 수정 — Order/OrderOption 모델 `$activityLogFields`의 status 필드를 `type: 'enum'`으로 변경 + `OrderStatusEnum::labelKey()` 메서드 추가" + ] + } + ] + }, + { + "version": "0.19.4", + "date": "2026-03-30", + "categories": [ + { + "name": "Changed", + "items": [ + "`window.G7Config` 설정 노출 최소화 — frontend_schema 필드별 `expose: false` 처리 (basic_info: route_path, no_route만 노출, language_currency: default_currency, currencies만 노출, order_settings/shipping/seo/review_settings 전체 미노출)" + ] + } + ] + }, + { + "version": "0.19.3", + "date": "2026-03-30", + "categories": [ + { + "name": "Fixed", + "items": [ + "관리자 다크모드 레이아웃 전수 수정 — 101개 파일에서 누락된 `dark:` variant 약 415건 추가 (text-gray, bg-white/gray, focus:ring, text-red/blue/green/yellow, border-gray, hover 등)", + "상품 일괄 변경 모달 라벨 미정의 CSS 클래스 수정 — `className: \"label\"` → `\"form-label\"` (다크모드 텍스트 가독성 복원)", + "상품 목록 판매가/재고 일괄 변경 버튼 다크모드 텍스트 색상 누락 수정 — `text-gray-700 dark:text-gray-300` 추가", + "레이아웃 JSON 내 Select 컴포넌트 다크모드 배경색 수정 — `dark:bg-gray-800` → `dark:bg-gray-700` (카드 배경과 구분)" + ] + } + ] + }, + { + "version": "0.19.2", + "date": "2026-03-30", + "categories": [ + { + "name": "Added", + "items": [ + "검색/필터/정렬 성능 향상을 위한 일반 인덱스 추가 — ecommerce_products(selling_price, list_price, stock_quantity, shipping_policy_id, barcode, tax_status, updated_at), ecommerce_orders(total_amount, order_device, confirmed_at, [user_id+ordered_at]), ecommerce_order_addresses(orderer_phone, recipient_phone)" + ] + }, + { + "name": "Changed", + "items": [ + "Repository 7개의 직접 `MATCH...AGAINST` 호출을 Laravel Scout `Model::search()` 파이프라인으로 전환 — DBMS별 검색 분기를 엔진 내부에서 처리 (ProductRepository, CategoryRepository, BrandRepository, CouponRepository, ProductCommonInfoRepository)", + "ReportRepository의 관계 검색을 `DatabaseFulltextEngine::whereFulltext()` 정적 헬퍼로 전환 — 다중 DBMS 호환", + "Category, Brand, PromotionCoupon, ProductCommonInfo 모델에 `FulltextSearchable` 인터페이스 + `Searchable` 트레이트 추가", + "`searchIndexShouldBeUpdated()` 훅 기반 전환 — `HookManager::applyFilters()` 사용으로 검색 플러그인이 모델별 인덱스 업데이트 제어 가능", + "마이그레이션 FULLTEXT 인덱스 생성을 `DatabaseFulltextEngine::addFulltextIndex()` 정적 헬퍼로 전환 — DBMS별 조건부 처리" + ] + }, + { + "name": "Fixed", + "items": [ + "인덱스명 약어 수정 — `idx_ecom_*` → `idx_ecommerce_*`, `stock_qty` → `stock_quantity` (네이밍 규칙 준수)" + ] + } + ] + }, + { + "version": "0.19.1", + "date": "2026-03-30", + "categories": [ + { + "name": "Added", + "items": [ + "상품 옵션 재생성 시 기존 옵션 초기화 확인 모달 연결 — 기존 옵션이 존재할 때 generateOptions 호출 시 확인 모달 표시, 확인 후 skipConfirm 플래그로 실제 생성 진행 (optionHandlers, _modal_confirm_regenerate.json)" + ] + } + ] + }, + { + "version": "0.19.0", + "date": "2026-03-30", + "categories": [ + { + "name": "Added", + "items": [ + "FULLTEXT 인덱스(ngram) 추가 — ecommerce_products(name, description), ecommerce_categories(name, description), ecommerce_brands(name), ecommerce_promotion_coupons(name, description), ecommerce_product_common_infos(name, content) 총 9개", + "Product 모델에 Laravel Scout `Searchable` 트레이트 + `FulltextSearchable` 인터페이스 적용" + ] + }, + { + "name": "Changed", + "items": [ + "상품/카테고리/브랜드/쿠폰/공통정보 관리자 검색 쿼리를 `LIKE '%keyword%'`에서 `MATCH...AGAINST IN BOOLEAN MODE`로 전환 — JSON TEXT 컬럼 검색 성능 향상", + "공개 상품 검색(통합검색) 쿼리를 로케일 루프 `JSON_EXTRACT + LIKE`에서 `MATCH...AGAINST`로 전환", + "관리자 상품 목록 검색 필드에 '상품설명', 'SKU' 옵션 추가 — description FULLTEXT 검색 지원" + ] + } + ] + }, + { + "version": "0.18.4", + "date": "2026-03-30", + "categories": [ + { + "name": "Fixed", + "items": [ + "상품 수정 시 이미지 순서 유실 수정 — syncImages에서 배열 인덱스를 sort_order SSoT로 사용, stale sort_order 값 무시 (ProductService)", + "상품 수정 시 대표이미지 hash 기반 설정 — syncImages에 thumbnail_hash 파라미터 추가, hash 기준 is_thumbnail 갱신 (ProductService)", + "상품 복사 시 원본 이미지 파일 복사 — copyFromSource() 메서드 추가, hash 기반 원본 조회 및 스토리지 파일 복사 (ProductImageService)", + "상품 복사 시 createImages에서 hash 기반 이미지 복사 분기 — hash 존재 시 copyFromSource 호출, 미존재 시 기존 직접 생성 (ProductService)", + "상품 이미지 임시 파일 연결 시 sort_order 재배치 — linkTempImages에서 기존 이미지 최대 sort_order 뒤에 배치, is_thumbnail 해제 (ProductImageService)", + "상품 수정 폼 이미지 메타데이터 확장 — download_url, size, size_formatted, mime_type, is_image, width, height, thumbnail_hash 추가 (ProductService)", + "상품 이미지 업로드 레이아웃 전면 수정 — event 기반 콜백, hash 기반 대표이미지, 편집/복사 모드별 API 엔드포인트 분기, onReorder/onRemove 핸들러 추가 (_partial_image_upload.json)", + "ProductResource에 thumbnail_hash 필드 추가 — 대표이미지 hash 반환 (ProductResource)", + "StoreProductRequest에 thumbnail_hash 검증 추가, description maxLength 65535 적용 (StoreProductRequest)", + "ProductController 에러 응답에 상세 에러 정보 추가 — ValidationException errors 전달, debug 모드 예외 메시지 포함 (ProductController)" + ] + } + ] + }, + { + "version": "0.18.3", + "date": "2026-03-30", + "categories": [ + { + "name": "Fixed", + "items": [ + "카테고리 삭제 모달 영문 다국어 ICU MessageFormat 오류 수정 — `{count, plural, ...}` 형식을 단순 `{count}` 치환 형식으로 변경 (category.json)" + ] + } + ] + }, + { + "version": "0.18.2", + "date": "2026-03-29", + "categories": [ + { + "name": "Fixed", + "items": [ + "주문 일괄 배송중 변경 시 운송장/택배사 정보가 저장되지 않는 버그 수정 — `bulkUpdateShipping()`에서 shipping 레코드 미존재 시 옵션별 신규 생성 로직 추가 (OrderRepository)" + ] + }, + { + "name": "Changed", + "items": [ + "order_shippings 테이블에서 `carrier_name` 컬럼 삭제 — 택배사명은 carrier_id 관계를 통해 사용자 로케일에 맞게 동적 조회로 전환 (OrderShipping, OrderOptionResource, OrderListResource, OrderShippingResource)" + ] + } + ] + }, + { + "version": "0.18.1", + "date": "2026-03-29", + "categories": [ + { + "name": "Fixed", + "items": [ + "배송완료 상태 변경 시 운송장 입력 강제 해제 — `shippingInfoRequiredStatuses()`에서 DELIVERED 제거 (OrderStatusEnum)", + "주문 목록 일괄 배송완료 시 상태가 \"배송중\"으로 표시되는 버그 수정 — `bulkUpdateShipping()`에서 order_status를 SHIPPING으로 덮어쓰는 코드 삭제 (OrderRepository)", + "주문상세 일괄변경 시 carrier vs carrier_id 키 불일치로 422 에러 발생 수정 — FE `body.carrier` → `body.carrier_id`, BE `$validated['carrier']` → `$validated['carrier_id']` (orderDetailHandlers, OrderController)", + "주문상세 옵션 일괄 상태 변경 후 부모 주문 상태 미동기화 수정 — `syncParentOrderStatus()` 메서드 추가, 모든 활성 옵션 동일 상태면 주문도 해당 상태로, 혼합 시 최저 진행 단계로 설정 (OrderOptionService)", + "ActivityLog 일괄 상태 변경 로그 미기록 수정 — Service results 키를 `order_option_id`로 변경하여 Listener pluck와 일치, 분할/병합 케이스별 loggable 대상 정확히 지정 (OrderOptionService, OrderActivityLogListener)", + "프론트엔드 주문 목록 배송완료 선택 시 운송장 필수 경고/비활성화 해제 — shippingStatuses 배열 및 modal 조건에서 delivered 제거 (orderHandlers, _modal_bulk_confirm.json)" + ] + }, + { + "name": "Changed", + "items": [ + "OrderOption 일괄 상태 변경 results 구조 확장 — `option_id` → `order_option_id`, `split_order_option_id`, `merged_into_order_option_id`, `is_full_quantity` 추가 (OrderOptionService)" + ] + } + ] + }, + { + "version": "0.18.0", + "date": "2026-03-29", + "categories": [ + { + "name": "Added", + "items": [ + "검색/필터/정렬 성능 향상을 위한 누락 인덱스 일괄 추가 — ecommerce_products 7개(selling_price, list_price, stock_quantity, shipping_policy_id, barcode, tax_status, updated_at), ecommerce_orders 4개(total_amount, order_device, confirmed_at, [user_id+ordered_at] 복합), ecommerce_order_addresses 2개(orderer_phone, recipient_phone)", + "상품 복사 모드 이미지 파일 복사 기능 — copyFromSource() 메서드로 원본 이미지 파일과 메타데이터를 새 상품에 복제 (ProductImageService)", + "상품 등록 시 에러 상세 로깅 — ValidationException/Exception 분리 처리 및 debug 모드 시 exception 메시지 포함 응답 (ProductController)" + ] + }, + { + "name": "Fixed", + "items": [ + "상품 등록 시 상세설명 255자 제한 버그 수정 — TranslatableField maxLength 65535 명시 (StoreProductRequest)", + "상품 복사/수정 화면에서 FileUploader 렌더링 에러 수정 — getDetailForForm() 이미지 매핑에 누락 필드(mime_type, is_image, download_url, size, order 등) 추가 (ProductService)", + "상품 복사 컨트롤러에서 중복 download_url 추가 로직 제거 — getDetailForForm()에서 이미 포함 (ProductController)", + "상품 복사 등록 시 500 에러 수정 — createImages()에서 hash 기반 복사 모드 이미지를 copyFromSource()로 파일 복사 처리 (ProductService)", + "인덱스 마이그레이션 날짜 순서 수정 — 테이블 생성 마이그레이션 이후로 재배치 + 인덱스 중복 방어 코드 추가", + "상품 수정/복사 화면에서 대표이미지 표시 안됨 수정 — id 기반 → hash 기반 대표이미지 식별로 전환, 복사 모드 이미지 삭제 시 원본 보호 (ProductResource, ProductService, _partial_image_upload.json)", + "상품 복사 등록 시 임시 이미지 정렬/썸네일 버그 수정 — linkTempImages()에서 sort_order 미설정(기존 이미지와 충돌) 및 is_thumbnail 미해제(이중 썸네일) 문제 해결 (ProductImageService)", + "상품 이미지 filesChange 이벤트가 form.images를 pendingFiles로 덮어쓰는 버그 수정 — 복사 이미지 데이터 유실 방지, uploadComplete도 form.images 미수정으로 createImages/linkTempImages 이중 처리 해소 (_partial_image_upload.json)" + ] + } + ] + }, + { + "version": "0.17.2", + "date": "2026-03-29", + "categories": [ + { + "name": "Fixed", + "items": [ + "주문상세 일괄변경 시 toast에 번역 키(`no_items_selected`)가 원문 그대로 표시되는 버그 수정 — 누락된 다국어 키 추가 (ko/en)", + "주문상세 일괄변경에서 \"선택 없음\"과 \"취소 가능 상품 없음\" 메시지를 동일 키로 사용하던 것을 `no_items_selected` / `no_cancellable_items`로 분리 (orderDetailHandlers)" + ] + } + ] + }, + { + "version": "0.17.1", + "date": "2026-03-28", + "categories": [ + { + "name": "Fixed", + "items": [ + "공통정보 추가 시 \"ko 언어의 값은 필수입니다\" 검증 오류 수정 — MultilingualInput의 setState에서 `$event` 전체 대신 `$event.target.value`로 다국어 값 객체만 저장", + "공통정보 뷰 패널 \"기본값으로 설정\" 버튼 미동작 수정 — 모달 ID 불일치(`set_default_confirm_modal` → `modal_set_default_confirm`) 및 확인 모달에 기본값 설정 API 호출 추가" + ] + } + ] + }, + { + "version": "0.17.0", + "date": "2026-03-30", + "categories": [ + { + "name": "Added", + "items": [ + "상품 상세 페이지에 1:1 문의 탭 추가", + "문의 게시판이 설정된 경우에만 탭 노출", + "비회원 포함 누구나 문의 목록 조회 가능, 로그인 사용자가 문의 작성/수정/삭제 가능", + "비밀글 토글 필터 (전체 / 비밀글 제외) 제공", + "관리자 권한 보유자가 문의에 답변 등록/수정/삭제 가능", + "작성자명 마스킹, 비밀글 잠금 처리", + "마이페이지에 내 문의내역 조회 API 추가 (답변 내용 포함)", + "이커머스 환경설정에 1:1 문의게시판 설정 섹션 추가", + "게시판 모듈과 연동하여 사용할 게시판을 지정할 수 있음", + "게시판 모듈 미설치 시에도 이커머스 단독 운영 가능", + "문의 접수/답변 완료 메일 템플릿 2종 추가 (`inquiry_received`, `inquiry_replied`)", + "문의 작성 시 상품명 다국어 스냅샷 저장 (상품 삭제 후에도 문의 내역에서 상품명 표시 유지)", + "기존 설치 환경을 위한 업그레이드 스텝 추가 (`Upgrade_0_9_0`)" + ] + }, + { + "name": "Changed", + "items": [ + "상품 복사 기능을 POST API 방식에서 navigate 패턴으로 전환 — 복사 옵션 선택 → 상품 등록 페이지로 이동하여 사전 입력 (역할 복사와 동일 UX)", + "복사 옵션 11개로 확장: 이미지, 옵션, 카테고리, 판매정보, 상품설명, 상품정보제공고시, 공통정보, 기타정보, 배송정책, SEO, 식별코드", + "수정/목록 페이지 복사/삭제 버튼을 setState 방식에서 openModal/closeModal 패턴으로 변경" + ] + }, + { + "name": "Fixed", + "items": [ + "문의 삭제 시 Action 훅이 트랜잭션 내부에서 실행되던 문제 수정 (롤백 시 부작용 방지)", + "훅명 표준화: 엔티티 부분의 하이픈(`-`)을 언더스코어(`_`)로 변경", + "마이페이지 문의 목록 조회 시 타입 불일치로 발생할 수 있는 오류 수정", + "문의 가능 여부(`inquiry_available`) 판단 기준을 게시판 slug 존재 여부로 통일", + "목록 페이지 복사 모달 제출 시 \"POST method not supported\" 에러 수정 — GET 라우트에 POST 요청 불일치 해결", + "수정 페이지 복사/삭제 버튼 미동작 수정 — modals 섹션에서 setState 플래그 대신 openModal 핸들러 사용", + "배송정책 목록 페이지에서 데이터 0건일 때 DataGrid와 Empty State가 동시 표시되는 버그 수정 — DataGrid, bulk_actions, table_header_bar, pagination에 조건부 렌더링 추가", + "배송정책 복사 시 원본 데이터가 폼에 로드되지 않는 버그 수정 — copy_source data_source 추가 및 initShippingPolicyForm 핸들러에 isCopy 모드 처리", + "배송정책 신규 등록/수정 시 is_default=true 설정 시 기존 기본 정책이 해제되지 않는 버그 수정 — create()/update()에 clearDefault() 호출 추가", + "배송정책 목록 DataGrid subRow에서 `$t:` 번역 토큰이 원문 그대로 표시되는 버그 수정 — `{{}}` 표현식 내 문자열 리터럴에서 `$t()` 함수 호출로 변경", + "배송정책 복사 모달이 실제 국가별 설정을 반영하지 않는 버그 수정 — 플랫 필드(배송방법, 운송사, 부과정책)에서 country_settings 기반 반복 렌더링으로 재설계" + ] + } + ] + }, + { + "version": "0.16.2", + "date": "2026-03-27", + "categories": [ + { + "name": "Fixed", + "items": [ + "카테고리 크로스 depth 이동 시 parent_id가 null로 변경되지 않는 버그 수정 — `isset()`이 null에 대해 false를 반환하여 `array_key_exists()`로 교체 (CategoryService)", + "이커머스 부모 메뉴 URL을 null로 변경 — 대응 라우트 없는 `/admin/ecommerce` URL이 404 발생 (module.php)" + ] + } + ] + }, + { + "version": "0.16.1", + "date": "2026-03-26", + "categories": [ + { + "name": "Fixed", + "items": [ + "상품 하위 라우트(form, copy, can-delete, logs) `whereNumber` 제약으로 product_code(영숫자) 404 발생 수정 — `where('product', '[0-9a-zA-Z]+')` 패턴으로 변경하여 ID/코드 둘 다 대응" + ] + } + ] + }, + { + "version": "0.16.0", + "date": "2026-03-26", + "categories": [ + { + "name": "Added", + "items": [ + "ProductLog → ActivityLog 통합: 상품 처리로그를 표준 ActivityLog 시스템으로 전환", + "ProductActivityLogListener: ActivityLog 표준 패턴 적용 (description_key, ChangeDetector)", + "ActivityLogDescriptionResolver: product_id → 상품명 실시간 해석", + "ProductController::logs(): 상품별 처리로그 API (OrderController::logs 패턴 동일)", + "Bulk Update ChangeDetector 지원: 일괄 변경 시 필드별 old/new 변경 내역 기록", + "OrderService: bulkUpdate, bulkUpdateStatus, bulkUpdateShipping에 스냅샷 캡처 + ChangeDetector 적용", + "OrderOptionService: bulkChangeStatusWithQuantity에 스냅샷 캡처 + ChangeDetector 적용", + "ProductOptionService: bulkUpdatePriceByMixedIds, bulkUpdateStockByMixedIds, bulkUpdate(통합)에 스냅샷 캡처 + ChangeDetector 적용", + "EcommerceAdminActivityLogListener: `option.after_bulk_update` 훅 구독 추가 (통합 일괄 수정 로깅)", + "ProductOptionRepositoryInterface: `findByIds()`, `getIdsByProductIds()` 메서드 추가", + "Listener 레벨 Bulk 스냅샷/ChangeDetector 전수 적용 (13건)", + "ProductActivityLogListener: bulk_update/bulk_price_update/bulk_stock_update before 스냅샷 + after ChangeDetector (6훅 추가)", + "CouponActivityLogListener: before_bulk_status 스냅샷 캡처 + after ChangeDetector 연동 (1훅 추가)", + "EcommerceAdminActivityLogListener: ExtraFeeTemplate bulk delete/toggle/create, ShippingPolicy bulk delete/toggle, ProductReview bulk delete 스냅샷 + ChangeDetector (9훅 추가)", + "삭제 작업 시 deleted_items 스냅샷 보존 (ExtraFeeTemplate, ShippingPolicy, ProductReview)", + "21개 모델에 $activityLogFields 정의 (코어 5개, 이커머스 11개, 게시판 4개, 페이지 1개)", + "Upgrade_0_16_0: ecommerce_product_logs 테이블 DROP + 레이아웃 캐시 클리어", + "활동 로그 Per-Item 로깅 전환: 모든 bulk 핸들러가 변경된 건별로 loggable_id 기록", + "OrderActivityLogListener: bulk_update/bulk_status_update/bulk_shipping_update per-Order, bulk_option_status_change per-OrderOption 전환", + "ProductActivityLogListener: bulk_update/bulk_price_update/bulk_stock_update per-Product 전환", + "CouponActivityLogListener: bulk_status per-Coupon 전환", + "EcommerceAdminActivityLogListener: ExtraFee/ShippingPolicy bulk toggle per-item, bulk_delete per-item (loggable_type/loggable_id 직접 지정) 전환", + "ShippingPolicyActivityLogListener: 중복 bulk 훅 구독 2개 삭제 (EcommerceAdminActivityLogListener에 위임)", + "EcommerceUserActivityLogListener: 사용자 주문생성(order.after_create) + 구매확인(order-option.after_confirm) 로그 추가", + "ActivityLogHandler: 삭제된 엔티티용 loggable_type/loggable_id 직접 지정 지원" + ] + }, + { + "name": "Removed", + "items": [ + "ecommerce_product_logs 테이블 (ActivityLog로 대체)", + "ProductLogService, ProductLogRepository, ProductLogController, ProductLog 모델 등 관련 코드 전량 삭제" + ] + }, + { + "name": "Changed", + "items": [ + "처리로그 레이아웃: log.description → log.localized_description (ActivityLogResource 기준)", + "Bulk Update 훅 시그니처: after_bulk_* 훅에 $allChanges 파라미터 추가 (하위 호환 유지)" + ] + } + ] + }, + { + "version": "0.15.0", + "date": "2026-03-26", + "categories": [ + { + "name": "Added", + "items": [ + "구매확정 기능 — 마이페이지 주문상세에서 옵션별 구매확정 API 및 UI", + "`confirmed_at` 컬럼 추가 (ecommerce_order_options 테이블)", + "OrderOption 모델: `confirmed_at` fillable/casts, `review()` HasOne 관계", + "ConfirmOrderOptionRequest: 소유권, 상태, 중복 확정 검증", + "OrderService.confirmOption(): 옵션 확정 + 전체 옵션 확정 시 주문도 CONFIRMED 전환", + "User OrderController.confirmOption(): 구매확정 API 엔드포인트", + "`sirsoft-ecommerce.order-option.before_confirm` / `after_confirm` 훅", + "OrderConfirmPointListener: 구매확정 시 포인트 적립 더미 리스너", + "리뷰 작성 기능 — 구매확정 후 리뷰 작성 모달 UI 및 이미지 순차 업로드", + "userConfirmOrderHandlers.ts: 구매확정 커스텀 핸들러", + "userReviewHandlers.ts: 리뷰 작성 + 이미지 순차 업로드 커스텀 핸들러", + "구매확정/리뷰 작성 모달 레이아웃 (_modal_confirm_purchase.json, _modal_write_review.json)", + "_items.json에 구매확정/리뷰작성/리뷰완료 버튼 조건부 렌더링 (PC + 모바일 동일)", + "OrderOptionResource에 `can_confirm`, `can_write_review`, `has_review` 필드 추가", + "환경설정: `confirmable_statuses` (주문설정), `review_settings` (리뷰설정) 추가", + "사용자 권한: `user-orders.confirm`, `user-reviews.write` 등록 (module.php + Upgrade_0_15_0)", + "리뷰 라우트에 `permission:user,sirsoft-ecommerce.user-reviews.write` 미들웨어 추가", + "다국어 키 추가 (ko/en): 구매확정/리뷰 관련 메시지, 예외, 프론트엔드 번역" + ] + }, + { + "name": "Changed", + "items": [ + "show.json: initLocal에 구매확정/리뷰 상태 변수 추가, modals에 구매확정/리뷰 모달 partial 등록", + "handlers/index.ts: confirmOrderOption, submitReview 핸들러 등록" + ] + }, + { + "name": "Fixed", + "items": [ + "상품폼 처리로그 설명 미표시 — ProductLogResource는 `description` 필드를 반환하나 레이아웃이 `localized_description`을 참조하여 빈 값 표시 (v0.13.0 변경 시 오류)" + ] + } + ] + }, + { + "version": "0.14.0", + "date": "2026-03-25", + "categories": [ + { + "name": "Added", + "items": [ + "클래임 사유 관리 시스템 — DB 기반 환불 사유 관리 (ClaimReason 모델, Repository, Service, Controller, FormRequest, Resource)", + "`ecommerce_claim_reasons` 테이블: type, code, name(다국어), fault_type, is_user_selectable, is_active, sort_order", + "Admin API 7개: 목록/생성/상세/수정/삭제/상태토글/활성목록 (`/admin/claim-reasons`)", + "User API 1개: 사용자 선택 가능 사유 목록 (`/user/claim-reasons`)", + "ClaimReasonFaultTypeEnum (customer/seller/carrier), ClaimReasonTypeEnum (refund)", + "쇼핑몰 환경설정 > 클래임 탭 — 환불 사유 인라인 편집 UI (배송사 관리 패턴)", + "테이블 뷰(PC) + 카드 뷰(모바일) 반응형 레이아웃", + "EcommerceSettingsController에 syncReasons() 연동", + "업그레이드 스크립트 (Upgrade_0_14_0) — 기존 CancelReasonTypeEnum 7개 값을 DB 시드 데이터로 마이그레이션" + ] + }, + { + "name": "Changed", + "items": [ + "주문 취소 사유를 하드코딩 Enum에서 DB 기반으로 전환", + "CancelReasonTypeEnum 삭제 → ClaimReason DB 조회로 대체", + "CancelOrderRequest (Admin/User): `Rule::in(enum)` → `Rule::exists(ecommerce_claim_reasons, code)`", + "OrderCancel 모델: Enum cast 제거, getRefundReasonLabel() 헬퍼 추가", + "OrderCancellationService: Enum 참조 제거, 단순 string 저장", + "관리자/사용자 취소 모달 — 하드코딩 Option에서 동적 iteration으로 변경", + "Admin 모달: refundReasons 데이터소스 + iteration 기반 동적 렌더링", + "User 모달: 동일 패턴 적용 (user/claim-reasons API 사용)" + ] + }, + { + "name": "Fixed", + "items": [ + "활동 로그 description 표시 시 번역 키가 원문 그대로 노출되는 문제 수정", + "ActivityLogDescriptionResolver 추가 — 표시 시점에 ID를 엔티티 이름으로 해석 (HookManager 필터 훅)", + "src/lang/{ko,en}/activity_log.php 번역 파일 동기화 (TranslationServiceProvider 로딩 경로 일치)", + "OrderActivityLogListener: handleOrderAfterSendEmail/handleOrderOptionAfterStatusChange description_params 불일치 수정", + "OrderOption의 product_option_name, option_name, option_value 미저장 및 다국어 미지원 수정 (OrderProcessingService, OrderOption)", + "createOrderOptions()에서 product_option_name/option_value 누락 → ProductOption 다국어 원본 직접 저장", + "3개 컬럼 varchar(255) → JSON 마이그레이션 + 모델 array cast 추가", + "Resource 3개(OrderOptionResource, OrderListResource, UserOrderListResource)에서 로케일 변환 추가", + "기존 문자열 데이터 하위호환 (is_array 체크)", + "StoreEcommerceSettingsRequest: claim/review_settings 탭 저장 실패 수정 — `_tab` validation 및 `validatedSettings()` 누락 보완", + "FormRequest 로케일 하드코딩 수정 — `name.ko` 직접 검증을 `LocaleRequiredTranslatable` Rule로 교체 (StoreEcommerceSettingsRequest, StoreProductRequest)" + ] + } + ] + }, + { + "version": "0.13.8", + "date": "2026-03-25", + "categories": [ + { + "name": "Fixed", + "items": [ + "옵션 레벨 promotions_applied_snapshot이 camelCase로 저장되는 버그 수정 — DTO `toArray()` 호출 누락으로 PHP 프로퍼티명이 그대로 직렬화됨 (OrderProcessingService)" + ] + } + ] + }, + { + "version": "0.13.7", + "date": "2026-03-25", + "categories": [ + { + "name": "Fixed", + "items": [ + "취소 모달 debounce 타이머에서 __g7ActionContext 미복원으로 모달 상태 업데이트 실패 수정 (cancelOrderHandlers, userCancelOrderHandlers)", + "debounce 콜백 실행 시 ActionDispatcher의 try/finally가 이미 __g7ActionContext를 복원하여 모달의 actionContext 사라짐", + "캡처/복원 패턴 적용으로 setLocal()이 모달의 actionContext.setState()를 정상 호출" + ] + }, + { + "name": "Changed", + "items": [ + "OrderActivityLogListener에서 명시적 log_type 지정 제거 — 코어 resolveLogType() 요청 경로 기반 판별로 대체 (6개 핸들러)" + ] + } + ] + }, + { + "version": "0.13.6", + "date": "2026-03-25", + "categories": [ + { + "name": "Fixed", + "items": [ + "toPreviewArray() refund_total 배송비 이중 계산: refundAmount에 이미 배송비 차이가 내포되어 있는데 shippingDifference를 별도 가산하여 환불 예정액 과다 표시 (AdjustmentResult)" + ] + }, + { + "name": "Added", + "items": [ + "refund_total 정합성 검증 테스트 4건: 부분취소 무료→유료 전환, 전체취소 배송비 포함, 쿠폰+배송비 복합 시나리오 (OrderAdjustmentServiceTest G-1~G-4)" + ] + } + ] + }, + { + "version": "0.13.5", + "date": "2026-03-25", + "categories": [ + { + "name": "Fixed", + "items": [ + "상품금액 정액 쿠폰 수량 미반영 수정: 정액 할인이 수량과 무관하게 1회만 적용되던 결함 → 적용 대상 옵션의 수량만큼 할인 적용 (OrderCalculationService.calculateCouponDiscount)", + "OrderSeeder 정액 상품쿠폰 수량 미반영 수정: 시더에서도 target_scope별 적용 대상 수량 계산하여 할인 적용 (OrderSeeder.getTargetQuantity)" + ] + }, + { + "name": "Added", + "items": [ + "부분취소 시 쿠폰 조건 미달로 결제금액 증가 시 취소 차단: validateRefundNotNegative 검증 + cancel_blocked 플래그 (OrderCancellationService, AdjustmentResult)", + "Admin/User 취소 모달에 취소 불가 경고 배너 + 취소 버튼 비활성화 (_modal_cancel_order.json, _modal_cancel.json)", + "수량별 정액 할인 테스트 16개 + cancel_blocked 테스트 4개 (OrderCalculationServiceTest, OrderAdjustmentServiceTest)" + ] + } + ] + }, + { + "version": "0.13.4", + "date": "2026-03-25", + "categories": [ + { + "name": "Fixed", + "items": [ + "쿠폰 수정 화면 적용 상품 테이블 데이터 미표시: CouponResource의 included_products/excluded_products에 product_code, name_localized, selling_price_formatted 필드 추가 (CouponResource.php)" + ] + } + ] + }, + { + "version": "0.13.3", + "date": "2026-03-25", + "categories": [ + { + "name": "Fixed", + "items": [ + "관리자 취소 모달 수량 변경 시 비교 테이블 미갱신 결함 수정: updateCancelQuantity 핸들러에 value 파라미터 누락 추가 (_modal_cancel_order.json)", + "관리자 취소 모달 onMount setState로 인한 모달/페이지 scope 단절 해소: onMount 제거 + 초기화 핸들러에서 상태 설정 및 estimateRefundAmount 호출 패턴으로 전환 (orderDetailHandlers.ts, _modal_cancel_order.json)" + ] + } + ] + }, + { + "version": "0.13.2", + "date": "2026-03-25", + "categories": [ + { + "name": "Fixed", + "items": [ + "상품수량(item_count) 스냅샷이 고유 상품 수(distinct)가 아닌 총 수량 합계로 계산되도록 수정 (captureOrderSnapshot)", + "상품쿠폰 최대할인금액 초과 결함 수정: 아이템별 할인 산출 후 글로벌 cap 비례 안분 적용 (applyProductCoupons 3단계 리팩토링)", + "CouponApplication 기록 시 존재하지 않는 필드($coupon->max_discount_amount) 대신 실제 DB 컬럼($coupon->discount_max_amount) 사용하도록 수정 (4곳)", + "buildCouponSnapshotsFromOrder()에 max_discount_amount 필드 추가하여 취소 재계산 시 쿠폰 최대할인 정보 보존" + ] + } + ] + }, + { + "version": "0.13.1", + "date": "2026-03-25", + "categories": [ + { + "name": "Added", + "items": [ + "취소 모달 주문금액 비교 테이블: 취소 전/후 금액 비교 표시 (상품수량, 총 정가금액, 상품/주문/배송비쿠폰 할인, 할인코드 할인, 기본/추가배송비, 포인트 사용, 과세금액, 부가세, 실결제금액, 적립예정포인트)", + "쿠폰 상세 표시: 상품쿠폰/주문쿠폰/배송비쿠폰별 적용 쿠폰명 + 할인금액 표시 (original_coupons, recalculated_coupons)", + "다통화 금액 표시: 총 정가금액/실결제금액 셀 하단에 다통화 금액 인라인 표시 (mc_original_snapshot, mc_recalculated_snapshot)", + "스냅샷 필드 확장: captureOrderSnapshot/captureRecalcSnapshot에 5개 필드 추가 (total_product_coupon_discount_amount, total_order_coupon_discount_amount, total_vat_amount, total_earned_points_amount, item_count)", + "AdjustmentResult DTO: mcOriginalSnapshot, mcRecalculatedSnapshot, originalCoupons, recalculatedCoupons 프로퍼티 추가", + "상품 소계 표시: 단가 × 수량 = 소계 형태로 변경 (Admin/User 모달 공통)", + "Admin 취소 모달 크기 1.5배 확대 (750px → 1125px), User 모달 크기 조정 (lg → 750px)", + "다국어 키 18개 추가 (ko/en, Admin ecommerce + User sirsoft-basic)" + ] + }, + { + "name": "Changed", + "items": [ + "취소 모달 \"환불 예정금액\" 섹션을 \"주문금액 비교\" 테이블로 전면 교체 (Admin/User 공통)", + "변경된 금액에 적색 하이라이트 적용 (text-red-600 dark:text-red-400 font-semibold)" + ] + } + ] + }, + { + "version": "0.13.0", + "date": "2026-03-25", + "categories": [ + { + "name": "Added", + "items": [ + "ActivityLog 리스너 전면 구현:", + "OrderActivityLogListener (주문 CUD + 상태변경 + 일괄처리 + 배송 + 이메일 + 취소/부분취소/환불)", + "CouponActivityLogListener (쿠폰 CUD + 일괄 상태변경)", + "ShippingPolicyActivityLogListener (배송정책 CUD + 토글 + 일괄처리)", + "CategoryActivityLogListener (카테고리 CUD + 순서변경 + 상태전환)", + "EcommerceAdminActivityLogListener (브랜드/라벨/공통정보/고시정보/추가비용/배송업체/상품옵션/상품이미지/리뷰 통합)", + "EcommerceUserActivityLogListener (장바구니/위시리스트/쿠폰사용/마일리지 — ActivityLogType::User)", + "활동 로그 다국어 키 100개 정의 (resources/lang/ko/activity_log.php, en/activity_log.php)", + "$activityLogFields 메타데이터: Product, Order, Coupon, ShippingPolicy 모델", + "ActivityLog 샘플 시더 (database/seeders/ActivityLogSampleSeeder.php)" + ] + }, + { + "name": "Changed", + "items": [ + "ProductActivityLogListener: ActivityLogService 호출 → Log::channel('activity') 직접 호출로 전환", + "주문상세 로그 탭 레이아웃: log.description → log.localized_description", + "상품폼 로그 섹션 레이아웃: log.description → log.localized_description" + ] + } + ] + }, + { + "version": "0.12.6", + "date": "2026-03-25", + "categories": [ + { + "name": "Added", + "items": [ + "OrderSeeder: 쿠폰 적용 주문 생성 — CouponIssue 테이블 조회 기반, 상품금액/주문금액 쿠폰 배분, promotions_applied_snapshot 스냅샷, 약 40% 주문에 쿠폰 적용", + "OrderSeeder: 주문 삭제 시 쿠폰 발급 상태 자동 복원 (used → available)", + "OrderSeeder: 배송비 쿠폰(shipping_fee) 적용 — 배송 레코드에 shipping_discount_amount 배분, 주문 합계에 배송 할인 반영", + "OrderSeeder: 결제완료 이상 상태 쿠폰 적용 비율 80%로 상향, 쿠폰 우선 배정 상태를 먼저 생성하여 쿠폰 재고 확보", + "OrderSeeder: payment_complete 비율 15→20%로 상향, 쿠폰 우선 상태의 비회원 비율 절반으로 축소" + ] + }, + { + "name": "Fixed", + "items": [ + "OrderSeeder: 주문 합계 재계산 시 쿠폰별 할인 필드(product_coupon, order_coupon, coupon, code) 정확히 합산하도록 개선", + "OrderSeeder: 주문 합계의 base_shipping_amount, shipping_discount_amount가 배송 레코드 실값을 반영하도록 수정", + "OrderSeeder: orders.promotions_applied_snapshot 저장 누락 수정 — 부분취소 시 쿠폰 재적용 불가 문제 해결 (coupon_issue_ids, product_promotions, order_promotions 구조)", + "OrderSeeder: 쿠폰 복원을 주문 삭제 전으로 이동 — order_id FK ON DELETE SET NULL로 인한 고아 쿠폰(status=used, order_id=null) 방지", + "OrderAdjustmentService: 복원 쿠폰 정보에서 다국어 쿠폰명(JSON 배열)을 문자열로 변환하지 않아 React 렌더링 오류 발생 — getLocalizedName() 사용으로 수정" + ] + } + ] + }, + { + "version": "0.12.5", + "date": "2026-03-25", + "categories": [ + { + "name": "Fixed", + "items": [ + "시더/팩토리 유령 할인 제거 — 출처 없는 랜덤 할인이 환불 재계산 시 잘못된 discount_difference 유발 (OrderSeeder, OrderOptionFactory, OrderFactory)", + "OrderSeeder: 결제 완료 주문의 mc_total_paid_amount가 항상 0이던 다중통화 불일치 수정" + ] + } + ] + }, + { + "version": "0.12.4", + "date": "2026-03-25", + "categories": [ + { + "name": "Added", + "items": [ + "사용자 주문 상품 선택 핸들러 추가 — toggleItemSelection, toggleSelectAllItems (userCancelOrderHandlers)", + "initUserCancelItems: 선택된 상품(_local.selectedItemIds) 기반으로 취소 대상 필터링" + ] + }, + { + "name": "Fixed", + "items": [ + "사용자 주문 취소 모달이 열리지 않던 버그 수정 — modals 섹션 모달에 id 추가 및 openModal/closeModal 패턴으로 전환 (userCancelOrderHandlers, _modal_cancel.json)", + "Upgrade_0_7_0: app(ModuleSettingsService::class) 직접 호출을 module_setting() 헬퍼로 교체" + ] + } + ] + }, + { + "version": "0.12.3", + "date": "2026-03-24", + "categories": [ + { + "name": "Fixed", + "items": [ + "OrderResource(주문상세 API)에 can_cancel ability 누락 — 사용자 주문상세 화면에서 취소 버튼이 절대 표시되지 않던 버그 수정", + "OrderResource에 resolveAbilities() override 추가 (상태 + 환경설정 + 권한 복합 조건)", + "Resource/Request에서 app(EcommerceSettingsService::class) 직접 호출을 module_setting() 헬퍼로 교체 (4개 파일)" + ] + }, + { + "name": "Added", + "items": [ + "주문상세 API abilities 테스트 3건 추가 (can_cancel true/false/권한 없음)" + ] + } + ] + }, + { + "version": "0.12.2", + "date": "2026-03-24", + "categories": [ + { + "name": "Fixed", + "items": [ + "주문 취소 시 환경설정(cancellable_statuses)을 참조하지 않고 하드코딩된 기본값만 사용하던 버그 수정", + "Admin/User CancelOrderRequest: EcommerceSettingsService에서 취소 가능 상태 조회", + "UserOrderListResource: can_cancel 판정에 환경설정 반영", + "OrderCancellationService: 상세 에러 메시지(현재 상태 + 취소 가능 상태 목록) 반환", + "프론트엔드 취소 핸들러에서 API 상세 에러 메시지를 토스트에 표시하도록 개선", + "주문 취소 시 취소 사유(reason) 미선택으로 검증 우회되던 버그 수정 (nullable → required)", + "취소 모달에서 422 validation 에러가 PG 에러 영역에 잘못 표시되던 버그 수정", + "422 → 필드별 에러 표시, 기타 에러 → PG 에러 영역으로 분리", + "CancelOrderRequest failedValidation에 필드별 에러(errors) 미포함 수정", + "취소 사유 필드 에러 메시지에 원시 필드명(reason) 노출 → 한국어 표시명(취소 사유) 적용" + ] + }, + { + "name": "Added", + "items": [ + "Order 모델에 getCancelDeniedReason(), getCancellableStatuses() 헬퍼 메서드 추가", + "취소 불가 상세 에러 다국어 키 추가 (order_not_cancellable_detail, order_already_cancelled)", + "환경설정 기반 취소 가능 상태 검증 테스트 4건 추가", + "취소 사유 필수 검증 테스트 추가", + "취소 모달 validation_error_title 다국어 키 추가 (ko/en)" + ] + }, + { + "name": "Changed", + "items": [ + "취소 모달 환불 우선순위 라디오: 마일리지 사용 주문 조건 제거 → 항상 표시" + ] + } + ] + }, + { + "version": "0.12.1", + "date": "2026-03-24", + "categories": [ + { + "name": "Fixed", + "items": [ + "취소 모달 배송비 정책명이 번역 키 그대로 노출되던 버그 수정 (OrderAdjustmentService)", + "OrderShipping에 shippingPolicy 관계 추가 (누락)", + "buildFullCancelResult() / buildShippingDetails()에서 ShippingPolicy::getLocalizedName() 사용", + "order.json에 default_shipping 번역 키 추가 (폴백용)" + ] + } + ] + }, + { + "version": "0.12.0", + "date": "2026-03-24", + "categories": [ + { + "name": "Added", + "items": [ + "배송비 계산 확장 훅: `sirsoft-ecommerce.shipping.calculate_fee` — 외부 확장이 배송비 계산을 오버라이드 가능", + "프로모션 스냅샷 저장 훅: `sirsoft-ecommerce.calculation.filter_promotions_snapshot` — 주문 생성 시 스냅샷에 확장 데이터 주입 가능", + "프로모션 스냅샷 복원 훅: `sirsoft-ecommerce.adjustment.filter_restore_promotions` — 환불 재계산 시 스냅샷에서 확장 데이터 해석/복원 가능", + "AppliedShippingPolicy DTO: `hookOverridden` 플래그 — 훅 오버라이드 배송비 식별용", + "스냅샷 기반 훅 오버라이드 배송비 보존: 환불 시 훅이 계산한 배송비를 스냅샷에서 복원 (훅 비활성 대응)", + "PluginExtensibilityTest: 16개 테스트 (배송정책 6, 할인정책 7, 복합 시나리오 3)", + "관리자/사용자 취소 모달 환불 예상금액 UI 확장: 배송비 정책별 상세, 잔여 PG/마일리지 잔액, 복원 쿠폰 목록, 총 환불액(PG+마일리지+배송비)", + "`changeRefundPriority` / `changeUserRefundPriority` 커스텀 핸들러: 환불 우선순위 변경 → debounce 재계산 연동", + "`refund_priority` 파라미터를 환불 예상금액 API에 전달 (관리자/사용자 공통)", + "mc_* 다중통화 환불 테스트 5건: 기본 변환, null 스냅샷, 마일리지 변환, 배송비 변환, previewArray 포함 확인", + "관리자 취소 모달 취소사유 라디오 7개 옵션 (CancelReasonTypeEnum 기반): order_mistake, changed_mind, reorder_other, delayed_delivery, product_info_different, admin_cancel, etc", + "i18n 취소사유 키 중첩 구조 변경: `cancel_reason_type_label` + `reason.{type}` 형태 (ko/en)", + "스냅샷 기반 환불 재계산 테스트 84건 추가 (B-1~B-7):", + "OrderAdjustmentServiceTest +33건 (쿠폰 복원 감지, 환불 우선순위, 배송비 상세, mc_* 변환, 복합 시나리오)", + "OrderCancellationServiceTest +17건 (쿠폰 복원 실행, 환불 처리, mc_* 레코드, 스냅샷 갱신)", + "OrderCalculationServiceTest +11건 (스냅샷 모드 검증)", + "OrderCalculationServiceMultiCurrencyTest +5건 (스냅샷 환율 변환)", + "Admin/User Feature 테스트 +12건 (환불 예상 API, 쿠폰 복원 DB)", + "프론트엔드 레이아웃 렌더링 테스트 +42건 (취소 모달 구조, 라디오, 조건부 렌더링)" + ] + }, + { + "name": "Fixed", + "items": [ + "`buildFullCancelResult`에 mc_* 다통화 변환, 배송비 상세, 복원 쿠폰 정보, 잔여 잔액, 환불 우선순위 누락 수정", + "`buildFullCancelResult`에서 OrderShipping 컬럼명 오류 수정 (`base_shipping_fee` → `base_shipping_amount`, `extra_shipping_fee` → `extra_shipping_amount`)" + ] + } + ] + }, + { + "version": "0.11.1", + "date": "2026-03-24", + "categories": [ + { + "name": "Added", + "items": [ + "OrderShippingRepository: 배송 모델 Repository 인터페이스 + 구현체 신규 생성", + "CouponIssueRepository: `update()`, `findByIds()`, `findByIdsWithRelations()` 메서드 추가", + "SnapshotProduct, SnapshotProductOption DTO: 스냅샷 기반 환불 재계산용", + "RefundPriorityEnum: 환불 우선순위 정의 (SHIPPING_DISCOUNT, ORDER_DISCOUNT, MILEAGE 등)", + "CouponRestoreListener: 주문 취소 시 쿠폰 사용 상태 자동 복원", + "OrderRefund 다통화 환불 컬럼 마이그레이션 (mc_refund_columns)", + "스냅샷 기반 환불 재계산 로직 (OrderCalculationService snapshot_mode, OrderAdjustmentService)", + "취소/환불 관련 예외 다국어 키 추가 (cancel_option_not_found, pg_refund_failed 등)", + "취소/환불 관련 Enum 다국어 라벨 추가 (CancelStatusEnum, RefundStatusEnum 등)", + "스냅샷 재계산 테스트 (SnapshotRecalculationTest), 쿠폰 복원 리스너 테스트 (CouponRestoreListenerTest)" + ] + }, + { + "name": "Fixed", + "items": [ + "Repository 패턴 위반 수정: 4개 서비스 + 1개 리스너의 직접 Eloquent 쿼리 → Repository 인터페이스 주입", + "CurrencyConversionService 예외 메시지 하드코딩 → `__()` 다국어 처리" + ] + }, + { + "name": "Changed", + "items": [ + "OrderAdjustmentService: CouponIssue 직접 접근 → CouponIssueRepositoryInterface 주입", + "OrderCancellationService: OrderShipping 직접 접근 → OrderShippingRepositoryInterface 주입", + "OrderOptionService: OrderShipping 직접 접근 → OrderShippingRepositoryInterface 주입", + "ShippingCarrierService: OrderShipping 직접 접근 → OrderShippingRepositoryInterface 주입", + "CouponRestoreListener: CouponIssue 직접 접근 → CouponIssueRepositoryInterface 주입", + "EcommerceServiceProvider: OrderShippingRepository 바인딩 등록" + ] + } + ] + }, + { + "version": "0.11.0", + "date": "2026-03-24", + "categories": [ + { + "name": "Added", + "items": [ + "주문 취소/환불 시스템: 관리자 전체취소·부분취소, 사용자 취소 요청, 환불 금액 산출", + "주문 취소 관련 모델 추가 (OrderCancel, OrderCancelOption, OrderRefund, OrderRefundOption)", + "주문 취소 Enum 추가 (CancelStatusEnum, CancelTypeEnum, CancelReasonTypeEnum, RefundStatusEnum, RefundMethodEnum 등)", + "주문 취소 서비스 추가 (OrderCancellationService, OrderAdjustmentService)", + "주문 취소 DTO 추가 (CancellationResult, CancellationAdjustment, OrderAdjustment, AdjustmentResult)", + "관리자 주문 취소 API 엔드포인트 (estimate-refund, cancel)", + "사용자 주문 취소 API 엔드포인트 (estimate-refund, cancel)", + "관리자 주문상세 취소 모달 레이아웃 (_modal_cancel_order.json)", + "사용자 마이페이지 주문 취소 모달 확장 (부분취소 지원)", + "주문 취소 프론트엔드 핸들러 (cancelOrderHandlers, userCancelOrderHandlers)", + "토스페이먼츠 환불 리스너 (PaymentRefundListener)", + "관리자 환경설정 주문취소 가능 상태 다중 선택 UI (_tab_order_settings.json)", + "관리자 주문상세 결제정보에 취소/환불 금액 표시 (_partial_payment_info.json)", + "주문관리 목록 주문자(회원) 검색 필터: SearchableDropdown 기반 UUID 검색", + "주문관리 DataGrid 주문자 클릭 시 회원/비회원 분기 검색 (회원: orderer_uuid, 비회원: search_keyword)", + "관리자 주문상세 일괄변경에서 주문취소 선택 시 PG 결제 취소 체크박스 인라인 표시" + ] + }, + { + "name": "Fixed", + "items": [ + "주문 취소 모달: 환불 예상금액 미표시 수정 (isolated 모달 스코프 상태 불일치 해결)", + "주문 취소 모달: 현재상태 컬럼에 enum 원시값 대신 다국어 라벨 표시", + "주문 취소 모달: PG 체크박스/라벨 클릭 연동 (htmlFor 추가)" + ] + }, + { + "name": "Changed", + "items": [ + "취소 상태 Enum 재설계: PENDING→REQUESTED, FAILED 제거 (2단계: 신청/완료)", + "환불 상태 Enum 재설계: 6단계 (신청/승인/처리중/보류/완료/반려), FAILED 제거", + "Option 상태를 부모 상태와 동일하게 동기화", + "OrderCancel 모델: isFailed() 메서드 제거", + "OrderRefund 모델: isFailed()→isRejected() 변경", + "PG 환불 실패 시 FAILED 상태 설정 대신 REQUESTED 유지 (에러 정보만 기록)", + "OrderStatusEnum: 주문취소(CANCELLED) 상태 추가", + "SequenceType: 취소(CANCEL), 환불(REFUND) 시퀀스 타입 추가", + "Order 모델: 취소 관련 관계 및 메서드 추가 (cancels, refunds, isCancellable 등)", + "OrderOption 모델: cancelled_quantity 컬럼 추가", + "OrderProcessingService: 취소 처리 로직 연동", + "관리자 OrderController: 취소/환불 산출 액션 추가", + "사용자 OrderController: 취소 요청 액션 추가" + ] + } + ] + }, + { + "version": "0.10.6", + "date": "2026-03-24", + "categories": [ + { + "name": "Changed", + "items": [ + "상품목록 API: 비활성 옵션도 포함하여 로드 (관리자 화면에서 비활성 옵션 표시)", + "상품상세 API: `includeInactive` 파라미터 추가 (관리자 수정 화면에서 비활성 옵션 포함)", + "옵션 일괄 변경 `option_name` 검증 규칙: `string` → `array` (다국어 배열 지원)", + "옵션 일괄 변경 `list_price` 검증 규칙 추가", + "일괄 변경 확인 모달: 처리중 상태 표시 (spinner + 버튼 비활성화)", + "상품 인라인 수정 필드 추적 확장 (`modifiedProductFields` 추가)", + "상품목록 DataGrid 선택 개수 표시 비활성화", + "주문목록 배송국가: 국가 코드(KR) → 다국어 국가명(한국) 표시 (OrderListResource, DataGrid)" + ] + }, + { + "name": "Fixed", + "items": [ + "시더(OrderAddressFactory)에서 `recipient_country_code` 기본값 누락으로 주문목록 배송국가 미표시 문제 수정", + "관리자 주문 수정 시 배송국가(`recipient_country_code`) 변경 불가 문제 수정 (OrderService, UpdateOrderRequest)", + "상품목록 리소스: `options` relation 로드 시 `option_stock_sum`, `options_count`가 빈 값으로 표시되는 문제 수정 (`relationLoaded` 분기 추가)", + "상품옵션 일괄 변경 후 상품 재고(stock_quantity)가 동기화되지 않는 문제 수정 (`ProductOptionService::bulkUpdate`에 `syncStockFromOptions` 호출 추가)", + "옵션 재고 합산 시 비활성 옵션이 포함되는 문제 수정 (프론트엔드 `updateOptionField`, `calculateTotalOptionStock` 핸들러에 `is_active` 필터 추가)", + "일괄 변경 확인 모달: 인라인 수정 시 `{method}` 플레이스홀더가 리터럴로 표시되는 문제 수정 (인라인 전용 다국어 키 분리)" + ] + } + ] + }, + { + "version": "0.10.5", + "date": "2026-03-23", + "categories": [ + { + "name": "Changed", + "items": [ + "상품수정 화면: 일부 미구현 탭 숨김 처리" + ] + }, + { + "name": "Fixed", + "items": [ + "UserAddressSeeder: name 필드에 다국어 배열 대입으로 인한 \"Array to string conversion\" 오류 수정 (단순 문자열로 변경)" + ] + } + ] + }, + { + "version": "0.10.4", + "date": "2026-03-23", + "categories": [ + { + "name": "Changed", + "items": [ + "주문상세 활동 로그: Badge → 아바타 원형+이름 스타일로 변경, 처리자 클릭 시 ActionMenu(회원정보 보기) 추가 (PC+모바일)", + "상품수정 활동 로그: \"작업\" 컬럼 제거, 처리자 클릭 시 ActionMenu(회원정보 보기) 추가 (PC+모바일)", + "주문상세/상품수정 활동 로그 UI 일관성 통일" + ] + }, + { + "name": "Fixed", + "items": [ + "상품수정 활동 로그: Select $event → $event.target.value, refreshDataSource → refetchDataSource, Pagination type:pageChange → event:onPageChange, 빈 상태 조건 경로 수정, 데이터소스 sort_order/per_page 파라미터 추가", + "주문상세 활동 로그: 동일 버그 6건 수정 (주문상세와 상품수정 패턴 통일)" + ] + } + ] + }, + { + "version": "0.10.3", + "date": "2026-03-23", + "categories": [ + { + "name": "Added", + "items": [ + "주문목록 주문상품 컬럼: 대표상품 썸네일, \"외 X건\" 표시, 상품수정 링크(새 창)", + "OrderListResource: first_option에 product_code 필드 추가" + ] + } + ] + }, + { + "version": "0.10.2", + "date": "2026-03-23", + "categories": [ + { + "name": "Added", + "items": [ + "주문상세 상품 이미지/상품명 클릭 시 상품수정 페이지로 이동 (새 창)" + ] + } + ] + }, + { + "version": "0.10.1", + "date": "2026-03-23", + "categories": [ + { + "name": "Changed", + "items": [ + "API Resource: user.id → user.uuid 전환 (OrderResource, OrderListResource, CouponResource, CouponIssueResource, ProductReviewResource, ProductLogResource, UserAddressResource, ExtraFeeTemplateResource)", + "user_id FK → user.uuid 전환 (CouponIssueResource, ProductReviewResource, UserAddressResource)", + "ExtraFeeTemplate: created_by → creator.uuid 전환 (creator 관계 활용)", + "쿠폰 관리 등록자 컬럼: eager-load에 uuid 컬럼 추가 (CouponRepository, ProductLogRepository)", + "관리자 레이아웃: user.id → user.uuid 참조 전환 (4개 파일)", + "orderHandlers.ts: localStorage 키에 user.uuid 사용", + "FormRequest: 정수 검증 → UUID 검증 전환 (CouponListRequest, CouponIssuesListRequest)", + "MergeCartOnLoginListener: Activity Log user_id → uuid 전환" + ] + } + ] + }, + { + "version": "0.10.0", + "date": "2026-03-22", + "categories": [ + { + "name": "Added", + "items": [ + "주문상세 이메일 발송 기능 — `POST /orders/{order}/send-email` API + 모달 UI 연동", + "SendOrderEmailRequest FormRequest — 이메일/메시지 검증", + "OrderService.sendEmail() — `Mail::raw()` + `HookManager::doAction()` 패턴 (코어 `sendTestMail()` 참조)", + "이메일 발송 다국어 메시지 (ko/en messages + validation + 프론트엔드 toast)" + ] + }, + { + "name": "Fixed", + "items": [ + "이메일 모달 보내기 버튼 무반응 — `actions` 배열 추가 (sequence: setState → apiCall → toast/closeModal)", + "이메일 모달 Textarea 높이 미적용 — `min-h-[100px]` (빌드 CSS 미존재) → `min-h-32` (safelist 존재)", + "이메일 모달 API 호출 시 `/undefined` 요청 — `params.endpoint` → `action.target` 수정", + "이메일 발송 실패 시 에러 원인 미표시 — catch 블록에 `$e->getMessage()` 상세 포함" + ] + }, + { + "name": "Changed", + "items": [ + "이메일 모달 `_global.isSending` → `_local.isSending` (모달 스코프 내 상태 관리)" + ] + } + ] + }, + { + "version": "0.9.3", + "date": "2026-03-22", + "categories": [ + { + "name": "Added", + "items": [ + "비회원 주문 Factory/Seeder 추가 — `OrderFactory::guest()` 상태, `OrderSeeder`에 비회원 주문 생성 로직", + "OrderOptionResource에 `list_price`, `list_price_formatted`, `final_amount`, `final_amount_formatted` 필드 추가", + "다국어 키 추가 — `phone_or_tel_hint` (ko/en)" + ] + }, + { + "name": "Fixed", + "items": [ + "주문상세 저장 버튼 미동작 — apiCall `params.endpoint` → `target` 속성, `auth_mode: \"required\"` 추가, onSuccess/onError 콜백 위치 수정", + "주문상세 수취인 정보 미표시 — `orderDetailHandlers.ts` 데이터 접근 경로 수정 (`_context.data.data` 우선 접근)", + "주문상세 수취인 정보 수정 값 미반영 — 폼 자동 바인딩 (`dataKey: \"form\"` + child `name`) 패턴 적용", + "Validation error 적색 테두리 미표시 — `.input` 클래스에 border 없음, `border border-red-500 dark:border-red-500` 으로 수정" + ] + }, + { + "name": "Changed", + "items": [ + "OrderService.update() 수취인 필드 저장 처리 추가 — shippingAddress 관계 업데이트", + "주문상세 폼 데이터 바인딩을 `initLocal` 패턴으로 변경 — 커스텀 핸들러(`initOrderDetailForm`) → 데이터소스 `initLocal`", + "UpdateOrderRequest validation 강화 — `recipient_name`, `recipient_zipcode`, `recipient_detail_address` 필수, `recipient_phone`/`recipient_tel` 택1 필수 (`required_without`)", + "CreateOrderRequest validation 통일 — `recipient_phone` required → `required_without:recipient_tel`, `address_detail` nullable → required", + "배송메시지 Textarea `textarea-sm` (80px), 관리자메모 Textarea `textarea-md` (128px) 크기 적용", + "OrderOptionResource `mc_subtotal_discount_amount` 제거 — 존재하지 않는 DB 필드 참조" + ] + } + ] + }, + { + "version": "0.9.2", + "date": "2026-03-22", + "categories": [ + { + "name": "Added", + "items": [ + "OrderResource에 `total_quantity`, `total_list_price`, `total_list_price_formatted` 필드 추가 — 합계행 백엔드 필드 기반 표시", + "주문상세 합계행(footerCells) 및 개별행에 다통화(보조 통화) 표시 — iteration 패턴으로 판매가/소계 하단에 표시" + ] + }, + { + "name": "Fixed", + "items": [ + "주문상세 합계행 판매가/수량/할인/적립 합계가 실제 데이터와 불일치 — computed 전면 제거 후 백엔드 필드로 대체", + "할인 0원에 마이너스 부호 표시(-0원) — 백엔드 `total_discount_amount > 0` 조건으로 수정", + "개별행 실구매가격이 할인 전 금액 표시 — `subtotal_price_formatted` → `final_amount_formatted`(할인 후)로 변경", + "OrderOptionResource의 `mc_subtotal_discount_amount` 버그 — 잘못된 필드(`mc_coupon_discount_amount`) 참조 수정" + ] + }, + { + "name": "Changed", + "items": [ + "관리자 주문상세 라우트를 ID 기준에서 order_number 기준으로 변경 — URL이 `/admin/ecommerce/orders/ORD-xxx` 형태로 표시 (상품 수정의 product_code 패턴과 동일)", + "Order 모델에 `resolveRouteBinding()` 추가 — 숫자는 ID, 문자열은 order_number로 자동 판별 (하위 호환)", + "API 라우트 `whereNumber('order')` 제약 제거 — order_number(문자열) 수용", + "주문 목록/상세/리뷰 레이아웃 및 커스텀 핸들러에서 navigate 경로를 order_number 기반으로 변경" + ] + } + ] + }, + { + "version": "0.9.1", + "date": "2026-03-21", + "categories": [ + { + "name": "Fixed", + "items": [ + "상품리뷰 정렬 기준 validation 수정 — sort 파라미터 `latest,oldest` → `created_at_desc,created_at_asc`로 통일 (PublicReviewListRequest)" + ] + } + ] + }, + { + "version": "0.9.0", + "date": "2026-03-21", + "categories": [ + { + "name": "Added", + "items": [ + "주문상세 수량 단위 상태 변경 기능 — 주문상품옵션의 일부 수량만 선택하여 상태 변경 (예: 3개 중 2개만 배송중으로 변경)", + "주문상품옵션 병합(Merge) 로직 — 분할 후 남은 수량을 같은 상태로 변경 시 자동 병합 (cascade 삭제 안전 처리 포함)", + "일괄변경 확인 모달에 상품 목록 표시 — 썸네일, 상품명/옵션/SKU, 판매가, 수량 편집 Input, 현재상태 Badge", + "updateChangeQuantityHandler 핸들러 추가 — 모달 내 수량 편집 시 1~최대수량 클램핑", + "다국어 키 5종 추가 — product_col, price_col, change_quantity_col, current_status_col, quantity_note (ko/en)", + "백엔드 테스트 15건 추가 — 금액 분할 정확성, 병합 로직, cascade 안전성, 스냅샷 보존, 순차 분할", + "프론트엔드 핸들러 테스트 12건 추가 — bulkConfirmItems 구성, 수량 클램핑, changeQuantities 반영" + ] + }, + { + "name": "Fixed", + "items": [ + "금액 분할 계산 2배 오류 수정 — replicate() 후 원본+복제본 금액이 동일하여 ratio 적용 시 2배로 계산되던 버그 (원본 금액 사전 캡처 방식으로 수정)" + ] + }, + { + "name": "Changed", + "items": [ + "buildOrderDetailBulkConfirmDataHandler 개선 — 선택 상품의 상세 정보(bulkConfirmItems)와 수량 맵(changeQuantities)을 _local에 저장 후 모달 열기", + "일괄변경 확인 모달 크기 lg로 확대 및 상품 목록 테이블 추가" + ] + } + ] + }, + { + "version": "0.8.3", + "date": "2026-03-21", + "categories": [ + { + "name": "Fixed", + "items": [ + "주문목록/주문상세 모달 9개 규정 미준수 전면 수정 — btn 클래스 → 명시적 Tailwind 클래스, 푸터 gap/mt 규정 준수, Icon name 규정 준수 (pen-to-square, trash)", + "모달 onError 핸들러 배열 형식 수정 — 객체 → 배열, error.message → $error.message (preset_save, preset_edit, preset_delete_confirm)", + "모달 액션 버튼 스피너/비활성화 처리 추가 — _global.isProcessing/isSaving/isDeleting/isDownloading/isSending 상태 기반 로딩 UI", + "Excel 다운로드 모달 meta.is_partial 누락 추가, 불필요한 props.id/closeOnOutsideClick/p-6 래퍼 제거" + ] + } + ] + }, + { + "version": "0.8.2", + "date": "2026-03-20", + "categories": [ + { + "name": "Changed", + "items": [ + "주문 일괄변경 드롭다운에서 pending_order(주문대기) 상태 제외 — 주문목록/주문상세 레이아웃 + 백엔드 FormRequest 검증 (BulkUpdateOrdersRequest)", + "주문상세 일괄변경 드롭다운에 누락된 상태 추가 — pending_payment, shipping_hold, confirmed, cancelled (pending_order 제외 전체 상태 표시)", + "주문 일괄변경 드롭다운 너비 확대(w-36 → w-44) — 주문목록/주문상세 공통, 한글 상태명 잘림 방지" + ] + }, + { + "name": "Fixed", + "items": [ + "주문상세 일괄변경 드롭다운 pending_payment 다국어 키 오류 수정 — order_status_options.pending_payment → order_status_options.payment_pending (enum 값과 다국어 키 불일치)" + ] + } + ] + }, + { + "version": "0.8.1", + "date": "2026-03-20", + "categories": [ + { + "name": "Added", + "items": [ + "OrderStatusEnum 배송 정보 필수 메서드 추가 — requiresShippingInfo(), shippingInfoRequiredStatuses(), shippingInfoRequiredValues()", + "주문 일괄변경 배송 상태(shipping_ready/shipping/delivered) 선택 시 택배사/송장번호 필수 검증 — 프론트엔드 핸들러(toast 경고), 레이아웃 모달(버튼 비활성화 + 경고 메시지), 백엔드 FormRequest 3계층 검증", + "BulkUpdateOrdersRequest carrier_id 존재 검증 추가 — Rule::exists(ShippingCarrier::class, 'id')", + "주문상세/주문관리 일괄변경 확인 모달에 배송정보 누락 경고 메시지 추가 (적색 텍스트)", + "다국어 키 추가 — carrier_required (ko/en), tracking_number_required (ko/en)", + "OrderOptionBulkStatusTest 배송정보 필수 검증 테스트 3건 추가" + ] + }, + { + "name": "Fixed", + "items": [ + "processOrderDetailBulkChangeHandler/saveAdminMemoHandler API 호출 수정 — G7Core.api.call() → G7Core.api.patch() (존재하지 않는 메서드 호출 오류)", + "processOrderDetailBulkChangeHandler 모달/디스패치 호출 수정 — G7Core.modal?.close?.() / G7Core.dispatch?.() 패턴 적용", + "processOrderDetailBulkChangeHandler 상태 조회 방식 변경 — getLocal() → action.params 직접 전달 (stale 상태 방지)" + ] + }, + { + "name": "Changed", + "items": [ + "BulkUpdateOrdersRequest/BulkChangeOrderOptionStatusRequest 배송 상태 검증을 하드코딩 → OrderStatusEnum::shippingInfoRequiredValues() 메서드로 리팩토링" + ] + } + ] + }, + { + "version": "0.8.0", + "date": "2026-03-20", + "categories": [ + { + "name": "Changed", + "items": [ + "OrderOptionStatusEnum 제거 및 OrderStatusEnum으로 통일 — 주문옵션 상태를 주문 상태와 동일한 Enum으로 관리", + "주문 일괄변경(bulkUpdate) 시 주문상품옵션 상태도 동일하게 일괄 변경 — 기존에는 orders 테이블만 업데이트" + ] + }, + { + "name": "Removed", + "items": [ + "OrderOptionStatusEnum 삭제 — OrderStatusEnum으로 완전 대체 (클레임 상태는 별도 모듈에서 지원 예정)", + "다국어 order_option_status 섹션 제거 — order_status 키 공용 사용" + ] + }, + { + "name": "Added", + "items": [ + "OrderRepository::bulkUpdateOptionStatus() 메서드 추가 — 주문 ID 배열 기준 옵션 상태 일괄 변경", + "OrderStatusEnum::isShipped() / shippedStatuses() 메서드 추가 — 발송 이후 상태 판별", + "OrderOption::shipped_quantity 접근자 추가 — 자신 및 분할 자식 옵션의 발송 수량 합산", + "업그레이드 스크립트 Upgrade_0_8_0 — option_status DB 값 변환 (pending→pending_order, shipped→shipping, 클레임→cancelled)" + ] + } + ] + }, + { + "version": "0.7.6", + "date": "2026-03-20", + "categories": [ + { + "name": "Changed", + "items": [ + "주문상세 주문상품 레이아웃을 CSS Grid → DataGrid 컴포넌트로 전환 — Tailwind purge 미적용 문제 해결, selectable/footerCells/footerCardChildren 활용", + "주문상세 합계 행을 DataGrid footerCells로 통합 — 기존 수동 CSS Grid 합계 영역 제거", + "주문상세 일괄변경 영역 스타일을 주문관리 목록과 동일하게 통일 — composite Select → basic Select, 배경/테두리/버튼 스타일 일치", + "주문상세 일괄변경 버튼을 buildOrderDetailBulkConfirmData 핸들러로 변경 — 검증(상태 미선택/상품 미선택) 후 모달 오픈 패턴 적용", + "주문상세 DataGrid selectedCountText를 빈 문자열로 설정 — 기본 \"N개 선택됨\" 텍스트 비활성화" + ] + }, + { + "name": "Fixed", + "items": [ + "주문상세 0원 항목 표시 — 할인/과세/부가세/쿠폰/할인코드/마일리지/예치금 금액이 0인 항목에 `condition` 속성 추가하여 숨김 처리", + "주문상세 합계 computed 문자열 연결 버그 — API 금액이 문자열(`\"4000.00\"`)로 반환되어 `reduce` 합산 시 문자열 연결 발생 → `Number()` 래핑으로 수정", + "주문상세 0원 금액 마이너스 표기 방지 — 금액 0원 항목이 `-0원`으로 표시되던 문제, condition 기반 숨김으로 해결", + "주문상세 일괄변경 버튼 클릭 무응답 — openModal 직접 호출 → buildOrderDetailBulkConfirmData 검증 핸들러로 교체", + "주문상세 일괄변경 확인 모달 closeModal 패턴 수정 — `params.target` → `target` 직접 지정", + "주문상세 DataGrid 셀 줄바꿈 방지 — 상품정보/헤더 제외 모든 셀과 footer에 whitespace-nowrap 적용" + ] + }, + { + "name": "Added", + "items": [ + "다국어 키 추가 — `no_products` (주문상품 없음) ko/en", + "buildOrderDetailBulkConfirmDataHandler 핸들러 추가 — 상태/상품 미선택 시 경고 토스트, 검증 통과 시 확인 모달 오픈", + "운송사/송장번호 입력 필드 조건부 표시 — 배송 관련 상태(shipping_ready/shipping/delivered) 선택 시에만 표시" + ] + } + ] + }, + { + "version": "0.7.5", + "date": "2026-03-20", + "categories": [ + { + "name": "Fixed", + "items": [ + "주문상세 헤더 \"상태:\" 텍스트 누락 — Badge 앞에 상태 라벨 Span 추가", + "주문상세 결제정보 금액 경로 오류 — `payment.*` 주문레벨 금액을 `order.data.*` 경로로 수정", + "주문상세 처리로그 더미 데이터 표시 — `_local.dummyLogs` → `order_logs` API 데이터소스 연동", + "주문상세 일괄변경 핸들러 변수명 불일치 — `batchCarrier` → `batchCarrierId`로 통일", + "OrderPaymentResource 필드명 5종 DB 컬럼 불일치 수정 (pg_tid→transaction_id, paid_amount→paid_amount_local, vbank_num_masked→vbank_number, vbank_due_date→vbank_due_at, cash_receipt_number→cash_receipt_identifier)" + ] + }, + { + "name": "Added", + "items": [ + "주문 처리 로그 API 엔드포인트 추가 (`GET /admin/orders/{id}/logs`) — ActivityLogService 연동, 페이지네이션/정렬 지원", + "Order 모델 payments HasMany 관계 추가 (기존 payment HasOne 유지)", + "OrderResource 플래튼 필드 추가 — 주문자(orderer_name/phone/email), 수취인(recipient_name/phone/zipcode/address), user_login_id, payments(복수), 세금(total_vat_amount)", + "OrderPaymentResource 추가 필드 — payment_type_label, payment_number, account_info, requested_at_formatted, due_date_formatted, vat_amount_formatted, card_approval_number, is_interest_free", + "OrderOptionResource option_name alias 추가", + "OrderService 활동 로그 기록 — update/bulkUpdate 시 ActivityLogService 연동", + "total_vat_amount 컬럼 마이그레이션 추가", + "처리로그 정렬/페이지당 옵션 변경 시 서버사이드 refetch 지원", + "다국어 키 추가 — 처리로그 정렬(date_asc), 페이지당 옵션(per_page_option), 로그 메시지 5종 (ko/en)", + "백엔드 테스트 3종 추가 (logs 엔드포인트 조회/페이지네이션/인증)", + "프론트엔드 레이아웃 테스트 갱신 — order_logs/active_carriers 데이터소스 검증, 금액 경로 검증, batchCarrierId 반영" + ] + } + ] + }, + { + "version": "0.7.4", + "date": "2026-03-19", + "categories": [ + { + "name": "Fixed", + "items": [ + "주문 목록 필터 5종 미작동 수정 — Repository 필터 키를 FormRequest 키와 정렬 (shipping_type, min_amount, max_amount, country_codes, order_device)", + "주문 목록 페이지네이션/총건수 미작동 — `orders?.data?.current_page` → `orders?.data?.pagination?.current_page` 경로 수정", + "배송국가 국기 미표시 — `row.country_code` → `row.address?.recipient_country_code` 경로 수정", + "OrderRepositoryTest 가격 필터 테스트 OLD 키 수정 (min_price → min_amount, max_price → max_amount)" + ] + }, + { + "name": "Added", + "items": [ + "구매환경(디바이스) 컬럼 추가 — OrderListResource에 `order_device`, `order_device_label` 필드 추가 + DataGrid 컬럼", + "첫구매 여부 표시 — OrderListResource에 `is_first_order` 필드 추가 + orderer 컬럼에 첫구매 뱃지", + "상품카테고리 4단계 계층 필터 — categories API 데이터소스 + computed 옵션 바인딩", + "배송국가 필터 동적화 — 하드코딩 6개국 → ecommerce_settings API 기반 iteration 렌더링", + "결제수단 필터 동적화 — 하드코딩 6종 → ecommerce_settings API 기반 iteration 렌더링", + "다국어 키 추가 — `column.device` (구매환경), `column.first_order` (첫구매) ko/en", + "백엔드 테스트 7종 추가 — 금액/국가/디바이스/배송방법 필터 + 페이지네이션 구조 + 신규 필드 확인", + "프론트엔드 레이아웃 테스트 29종 — 페이지네이션 경로, 필터 키, 컬럼, ActionMenu, 날짜 포맷, DataGrid 설정, 동적 필터, 클레임 제거 검증" + ] + }, + { + "name": "Changed", + "items": [ + "페이지 헤더 아이콘 제거 (디자인 통일)", + "기간 버튼 CSS `btn btn-xs` → `btn-date` 패턴 통일 (상품관리와 동일)", + "NO 컬럼 기본 숨김 (visibleColumns에서 제거)", + "주문자명 클릭 시 직접 navigate 제거 — 셀 레벨 ActionMenu로 변경 (쿠폰관리와 동일 패턴)", + "주문일시 날짜 표기 ISO → formatted 형식으로 수정 (`ordered_at` → `ordered_at_formatted`)", + "DataGrid 컬럼선택기 활성화 (`showColumnSelector: true`) + `responsiveBreakpoint: 768` 모바일 대응 추가" + ] + }, + { + "name": "Removed", + "items": [ + "클레임상태 필터 전체 제거 (state, data_sources params, init_actions, filter UI)" + ] + } + ] + }, + { + "version": "0.7.3", + "date": "2026-03-18", + "categories": [ + { + "name": "Added", + "items": [ + "`WishlistResource` + `WishlistCollection` 신규 생성 — 위시리스트 API에 표준 ResourceCollection 패턴 적용", + "`EcommerceSettingsService::getPublicPaymentSettings()` 메서드 추가 — 은행명 매핑 로직을 Service로 이동" + ] + }, + { + "name": "Changed", + "items": [ + "Public 컨트롤러 4개 `ResponseHelper` 패턴 일관성 수정 — `moduleSuccess`/`moduleError` 통일", + "`WishlistController`: 이중 번역 버그(`__()` + `success()`) 7곳 수정", + "`CartController`: `success('sirsoft-ecommerce::...')` → `moduleSuccess()` 10곳 수정", + "`CheckoutController`: success/error 패턴 혼재 6곳 수정", + "`WishlistController::index()` 인라인 `through()` 변환 → `WishlistCollection` 적용", + "`EcommerceSettingsController::payment()` 은행명 매핑 로직 → `EcommerceSettingsService`로 이동", + "`CartController`: `issueCartKey()`, `count()`에 `logApiUsage` 추가", + "`EcommerceSettingsController`: 3개 메서드에 `logApiUsage` 추가", + "`CheckoutController`: PHPDoc `@param` 타입 3곳 실제 FormRequest 타입으로 수정", + "`CategoryImageController::download()` 리턴 타입 선언 추가" + ] + } + ] + }, + { + "version": "0.7.2", + "date": "2026-03-18", + "categories": [ + { + "name": "Fixed", + "items": [ + "Public 상품 목록 API에 `ProductCollection` 패턴 적용 — 페이지네이션/abilities 포함 응답 구조 정상화", + "`ProductCollection` 콜백에서 `toArray()` → `resolve()` 변경 — 중첩 Resource(MissingValue) 안전 해석" + ] + }, + { + "name": "Changed", + "items": [ + "`TestingSeeder`에 `guest` 역할 및 사용자 타입 권한 추가 — Public API 테스트에서 PermissionMiddleware 통과 보장" + ] + } + ] + }, + { + "version": "0.7.1", + "date": "2026-03-18", + "categories": [ + { + "name": "Added", + "items": [ + "SEO 설정 변경 리스너 (`SeoSettingsCacheListener`) — 이커머스 모듈 SEO 설정 변경 시 영향받는 레이아웃 캐시를 선별적으로 무효화" + ] + }, + { + "name": "Changed", + "items": [ + "`SeoProductCacheListener` 보강 — 상품 수정 시 `home`/`search/index` 캐시 무효화 추가, 생성/수정 시 상세 페이지 캐시 즉시 재생성", + "`SeoCategoryCacheListener` 보강 — `search/index` 캐시 무효화 추가", + "배송지 `name` 필드를 다국어 JSON에서 단순 문자열로 변환 — 마이그레이션, 모델, 팩토리, 리포지토리, 서비스, FormRequest, 테스트 일괄 수정" + ] + } + ] + }, + { + "version": "0.7.0", + "date": "2026-03-17", + "categories": [ + { + "name": "Changed", + "items": [ + "SEO 설정 중 코어 범위 항목을 코어 환경설정으로 이관 (`seo_user_agents` → 코어 `seo.bot_user_agents`)", + "이커머스 SEO 탭에서 메인화면 관련 설정 제거 (`meta_main_title`, `meta_main_description`, `seo_site_main`)", + "이커머스 SEO 탭에서 User-Agent 관리 섹션 제거 (코어로 이관)" + ] + }, + { + "name": "Added", + "items": [ + "업그레이드 스텝 (`Upgrade_0_7_0`) — 기존 사용자 정의 User-Agent를 코어로 자동 이관" + ] + }, + { + "name": "Removed", + "items": [ + "`defaults.json`에서 `seo_user_agents`, `seo_site_main`, `meta_main_title`, `meta_main_description` 필드 제거", + "`StoreEcommerceSettingsRequest`에서 제거된 필드의 검증 규칙 삭제", + "SEO 탭 레이아웃에서 메인화면 메타 아코디언, 사이트 메인 체크박스, User-Agent 관리 섹션 제거" + ] + } + ] + }, + { + "version": "0.5.5", + "date": "2026-03-18", + "categories": [ + { + "name": "Added", + "items": [ + "상품 리뷰 기능 구현", + "DB: 리뷰 테이블, 리뷰 이미지 테이블 생성. 답변 수정일 컬럼 추가", + "모델/서비스: 리뷰 작성 가능 여부 확인, 리뷰 생성·삭제, 상태 변경, 판매자 답변 등록·수정·삭제, 이미��� 업로드·삭제", + "관리자 API: 리뷰 목록 조회(기간·별점·상태·포토리뷰·답변 여부 필터), 상태 변경, 삭제, 일괄 처리, 판매자 답변 CRUD", + "유저 API: 리뷰 작성 가능 여부 확인, 리뷰 작성·삭제, 이미지 업로드·삭제 (최대 5장)", + "공개 API: 상품별 리뷰 목록 (별점 통계, 옵션 필터 포함)", + "관리자 레이아웃: 리뷰 목록 화면 (검색 필터, 답변 모달, 이미지 미리보기 모달, 일괄 처리)", + "관리자 설정: 리뷰 작성 기한, 포토리뷰 최대 장수 설정 탭 추가", + "권한 및 메뉴: 관리자 사이드바 \"상품 리뷰 관리\" 메뉴 등록", + "다국어: 리뷰 관련 다국어 파일 추가 (한국어/영어)", + "샘플 시더: 별점 분포·답변·포토리뷰·답변 수정일 랜덤 생성", + "테스트: 관리자·유저·이미지 Feature 테스트 57개, 레이아웃 렌더링 테스트 94개", + "상품 목록 API에 평균 별점, 리뷰수 필드 추가" + ] + }, + { + "name": "Fixed", + "items": [ + "리뷰 작성 가능 여부 확인 시 구매확정 상태 비교가 항상 실패하던 버그 수정", + "리뷰 저장 시 옵션 스냅샷 배열 직렬화 오류 수정", + "리뷰 이미지 업로드 검증 실패 시 응답 코드가 400으로 반환되던 버그 수정 (422으로 수정)", + "리뷰 이미지 API 응답에 리뷰 ID 누락 수정" + ] + } + ] + }, + { + "version": "0.5.4", + "date": "2026-03-17", + "categories": [ + { + "name": "Fixed", + "items": [ + "주문 상세 SMS/이메일 발송 버튼 openModal 핸들러 포맷 수정 (`params.target` → `target`)" + ] + } + ] + }, + { + "version": "0.5.3", + "date": "2026-03-16", + "categories": [ + { + "name": "Fixed", + "items": [ + "쿠폰 리스트 DataGrid `cellChildren` 액션에서 등록자 필터 검색 시 SearchableDropdown에 선택값이 표시되지 않는 버그 수정 — `setState(target: \"local\")`가 globalStateUpdater 경로로 실행되어 Form stale 값에 덮어쓰이는 문제 (템플릿 DataGrid componentContext 수정으로 해결)", + "CouponResource에 카테고리 로케일 브레드크럼 경로 표시 추가", + "쿠폰 1인당 발급 제한 `per_user_limit` null 오류 수정 — nullable 처리 및 검증 로직 보완", + "쿠폰 할인율 검증 수정 — 정률 할인 시 100% 초과 방지, 정액 할인 시 할인율 검증 제거", + "쿠폰 유효기간/발급기간 타임존 변환 구현 — `TimezoneHelper`를 통한 사용자 타임존 ↔ UTC 변환", + "쿠폰 발급기간 `datetime-local` 입력 변환 및 `CouponResource` 타임존 출력 수정", + "쿠폰 폼 레이아웃에서 `apiCall` 호출 시 `auth_mode` 누락 수정" + ] + }, + { + "name": "Changed", + "items": [ + "쿠폰 폼 상품 검색을 무한스크롤 방식으로 개선", + "쿠폰 폼 레이아웃에서 사용 조건 Partial 분리", + "마이그레이션 통합 — 증분 마이그레이션을 테이블당 1개 create 마이그레이션으로 정리", + "시더 디렉토리 분리 — 샘플 시더를 `Sample/` 하위로 이동", + "라이선스 프로그램 명칭 정비", + "쿠폰 폼 하단 버튼 영역 제거 — 상단 PageHeader 액션으로 통일" + ] + } + ] + }, + { + "version": "0.5.1", + "date": "2026-03-11", + "categories": [ + { + "name": "Added", + "items": [ + "주문 취소 API 엔드포인트 추가 (`POST /user/orders/{id}/cancel`) + `user-orders.cancel` 권한 미들웨어 적용", + "`CancelOrderRequest` — 주문 취소 요청 검증 (소유자 확인, 취소 가능 상태 검증)", + "`UserOrderListResource` — `abilities` 키 추가 (`can_view`, `can_cancel` — 상태+권한 기반)", + "`UserOrderCollection` — `abilities` 키 추가 (`can_create` — 권한 기반)", + "`UserAddressResource` — `abilities` 키 추가 (`can_update`, `can_delete`, `can_set_default`)", + "`UserAddressCollection` — `abilities` 키 추가 (`can_create`)", + "`EcommerceUserAbilitiesTest` — 주문/배송지 abilities 및 주문 취소 API 테스트 13개", + "manifest에 license 필드 및 LICENSE 파일 추가" + ] + } + ] + }, + { + "version": "0.5.0", + "date": "2026-03-11", + "categories": [ + { + "name": "Added", + "items": [ + "사용자 권한 3개 추가 (블랙컨슈머 차단용)", + "`sirsoft-ecommerce.user-products.read`: 상품 조회 권한", + "`sirsoft-ecommerce.user-orders.create`: 주문하기 권한", + "`sirsoft-ecommerce.user-orders.cancel`: 주문 취소 권한", + "상품 조회 라우트에 `optional.sanctum` + `permission:user` 미들웨어 적용", + "주문 생성 라우트에 `permission:user` 미들웨어 적용", + "주문 배송지 변경 API (`PUT /user/orders/{id}/shipping-address`)", + "주문 상세 배송지 변경 모달 (주문상세 화면)", + "배송지명 중복 덮어쓰기 확인 모달", + "업그레이드 스크립트 `Upgrade_0_5_0.php` (권한 생성 + 기존 역할 할당)", + "업그레이드 스크립트 `Upgrade_0_6_0.php` (배송지명 컬럼 검증 + 캐시 클리어)", + "사용자 권한 미들웨어 테스트 `EcommerceUserPermissionTest` (7개)" + ] + }, + { + "name": "Changed", + "items": [ + "배송지명(`name`) 필드 다국어 JSON → 단순 string 변환 (마이그레이션 포함)" + ] + }, + { + "name": "Fixed", + "items": [ + "마이페이지 배송지 관리 API 엔드포인트 및 필드명 불일치 수정", + "체크아웃 배송지 모달 필드명 불일치 수정", + "무통장입금(dbank) 주문 시 결제수단이 'card'로 잘못 전송되던 버그 수정", + "도서산간 추가배송비 제주도 누락 수정", + "배송지 변경 시 배송비 미재계산 수정", + "배송지 모달 폼 전송 실패 수정", + "주문 완료 후 장바구니 아이템이 삭제되지 않던 버그 수정 (재고 차감 타이밍과 동기화)" + ] + } + ] + }, + { + "version": "0.4.2", + "date": "2026-03-10", + "categories": [ + { + "name": "Changed", + "items": [ + "권한 카테고리에 `resource_route_key`, `owner_key` 스코프 메타데이터 추가 (products, orders, brands, promotion-coupon, shipping-policies)", + "라우트 미들웨어 `except:owner:*` 옵션 제거 (scope_type 데이터 기반 시스템으로 전환)", + "Repository 목록 조회에 `PermissionHelper::applyPermissionScope()` 적용 (Product, Order, Brand, ShippingPolicy, Coupon)", + "`PermissionBypassable` 인터페이스 및 `getBypassUserId()` 제거 (Product, Order, Brand, ShippingPolicy, Coupon 등 모델)" + ] + } + ] + }, + { + "version": "0.4.1", + "date": "2026-03-08", + "categories": [ + { + "name": "Changed", + "items": [ + "API 리소스 권한 플래그 키 `permissions` → `abilities`로 변경 (코어 표준화)", + "`permissionMap()` → `abilityMap()` 메서드명 변경 (11개 리소스)", + "`BrandCollection`, `CategoryCollection`, `ShippingCarrierCollection`에 `HasAbilityCheck` 트레이트 적용", + "컬렉션 레벨 권한 응답 키를 `abilities`로 통일", + "관리자 레이아웃 JSON의 `permissions.can_*` 바인딩을 `abilities.can_*`로 변경" + ] + } + ] + }, + { + "version": "0.4.0", + "date": "2026-03-06", + "categories": [ + { + "name": "Changed", + "items": [ + "메일 템플릿 목록 API에 검색/필터/페이지네이션 지원 추가", + "메일 템플릿 편집 모달 UX 개선 (blur_until_loaded, sticky footer, 함수형 body)", + "`getDefaultTemplateData()`를 Controller에서 Service로 이동 (Service-Repository 패턴 준수)", + "메일 템플릿 탭 UI를 코어 환경설정과 동일한 구조로 변경" + ] + }, + { + "name": "Added", + "items": [ + "메일 템플릿 미리보기(preview) API 엔드포인트 추가", + "메일 템플릿 검색 기능 (제목/본문/전체)", + "페이지당 항목 수 선택 및 페이지네이션", + "메일 템플릿 관련 다국어 키 추가 (검색/필터/빈 상태/편집 모달)" + ] + } + ] + }, + { + "version": "0.3.1", + "date": "2026-03-06", + "categories": [ + { + "name": "Fixed", + "items": [ + "`CategoryService::deleteCategory()` — 카테고리 삭제 시 이미지(`images()`) 명시적 삭제 추가 (DB CASCADE 의존 제거)", + "`OrderService::delete()` — 주문 삭제 시 관계 레코드 5건 명시적 삭제 추가 (`taxInvoices`, `shippings`, `addresses`, `payment`, `options`)" + ] + } + ] + }, + { + "version": "0.3.0", + "date": "2026-03-06", + "categories": [ + { + "name": "Added", + "items": [ + "사용자 쿠폰 다운로드 API (`UserCouponController`) — 다운로드 가능 쿠폰 목록 조회/다운로드", + "상품별 다운로드 가능 쿠폰 공개 API (`PublicCouponController`) — 비로그인 사용자도 조회 가능", + "`UserCouponService` — 쿠폰 다운로드 비즈니스 로직 (중복 다운로드 방지, 수량 검증)", + "`CouponRepository`, `CouponIssueRepository` — 쿠폰 데이터 접근 계층", + "`DownloadCouponRequest` — 쿠폰 다운로드 요청 검증", + "쿠폰 다운로드 관련 다국어 메시지 (ko/en `messages.php`, `validation.php`)", + "사용자 쿠폰 다운로드 Feature 테스트 14건 (UserCouponControllerTest + PublicCouponControllerTest)", + "DB 기반 메일 템플릿 시스템 (5종: 주문 확인, 배송 시작, 구매 확정, 주문 취소, 관리자 신규 주문)", + "`ecommerce_mail_templates` 테이블, EcommerceMailTemplate 모델/서비스/리포지토리/컨트롤러", + "메일 템플릿 관리 API 및 환경설정 UI", + "업그레이드 스텝 (`Upgrade_0_3_0`) — 기존 설치에 메일 템플릿 초기 시딩", + "`getSeeders()`에 `EcommerceMailTemplateSeeder` 추가" + ] + } + ] + }, + { + "version": "0.2.1", + "date": "2026-03-03", + "categories": [ + { + "name": "Added", + "items": [ + "도서산간 추가배송비 템플릿 시더 (`ExtraFeeTemplateSeeder`) — 34건 도서산간 우편번호 데이터", + "도서산간 템플릿 모달 CRUD 기능 (생성/수정/삭제/일괄삭제/검색)", + "템플릿 모달에서 전체 적용/선택 적용 기능", + "백엔드 다국어 메시지 (`extra_fee_template` 섹션) 추가 (ko/en)" + ] + }, + { + "name": "Changed", + "items": [ + "도서산간 템플릿 모달 크기 `md` → `xl` 확대", + "도서산간 템플릿 모달을 API 응답 구조에 맞게 전면 재설계", + "`zipcode` 컬럼 `string(10)` → `string(20)` 확장 (범위 형식 지원)", + "FormRequest zipcode 검증 `max:10` → `max:20` 변경", + "부모 레이아웃에 템플릿 CRUD 상태 및 데이터소스 검색 params 추가" + ] + } + ] + }, + { + "version": "0.2.0", + "date": "2026-03-03", + "categories": [ + { + "name": "Added", + "items": [ + "배송사 관리 기능 신설 (CRUD API + 관리 UI)", + "배송사 마스터 테이블 (`ecommerce_shipping_carriers`) 생성", + "배송 설정 탭에 배송사 관리 인라인 섹션 추가", + "주문 배송 추적 URL 생성 (`OrderShipping.getTrackingUrl()`)", + "해외배송 비활성화 확인 모달 추가", + "배송가능국가 테이블에 국기 아이콘 표시", + "업그레이드 스텝 (`Upgrade_0_2_0`) — 배송사 시딩 + 기존 주문 carrier_id 역매핑" + ] + }, + { + "name": "Changed", + "items": [ + "해외배송 토글 수동 바인딩 전환 (해외배송 OFF 시 국가 추가/토글/삭제 비활성화)", + "주문 레이아웃 배송사 옵션을 하드코딩에서 API 기반 동적 로딩으로 전환", + "모듈 설치 시더에 `ShippingCarrierSeeder` 추가" + ] + }, + { + "name": "Removed", + "items": [ + "`CarrierEnum` 삭제 — 배송사를 하드코딩 Enum에서 DB 기반 동적 관리로 전환", + "배송정책에서 carrier 필드 제거 (`shipping_policy_country_settings.carrier` 컬럼 삭제)", + "배송정책 폼/목록에서 carrier Select 및 필터 UI 제거" + ] + } + ] + }, + { + "version": "0.1.7", + "date": "2026-02-27", + "categories": [ + { + "name": "Fixed", + "items": [ + "배송정책 수정 폼에서 가시성 플래그가 잘못 설정되는 버그 수정 (init_actions if 미지원 대응)", + "구간별 배송비 정책 수정 시 구간 테이블 미표시 및 고정배송비 필드 오표시 문제 해결", + "배송정책명(MultilingualInput) 수정 모드 미표시 문제 해결 (dataKey 패턴 적용)" + ] + }, + { + "name": "Changed", + "items": [ + "배송정책 폼에 dataKey=\"form\" + debounce 패턴 적용 (상품 폼과 동일 구조)", + "단순 setState 핸들러 제거 (shipping_method, currency_code → 자동 바인딩)" + ] + } + ] + }, + { + "version": "0.1.2", + "date": "2026-02-25", + "categories": [ + { + "name": "Changed", + "items": [ + "모듈 라우트 admin/user 분기 서빙 적용 (routes.json → routes/admin.json 이동)" + ] + } + ] + }, + { + "version": "0.1.1", + "date": "2026-02-24", + "categories": [ + { + "name": "Changed", + "items": [ + "버전 체계 조정 (정식 출시 전 0.x 체계로 변경)" + ] + } + ] + }, + { + "version": "0.1.0", + "date": "2026-02-23", + "categories": [ + { + "name": "Added", + "items": [ + "이커머스 모듈 초기 구현", + "상품 관리: CRUD, 옵션(사이즈/색상), 라벨, SEO 메타, 이미지 갤러리", + "상품 카테고리: 계층형 카테고리, 순서 관리, TreeView 편집", + "브랜드 관리: CRUD, 로고, 설명", + "쿠폰 시스템: 정액/정률 할인, 사용 조건 (최소 금액, 특정 상품), 유효기간, 사용 횟수 제한", + "배송 정책: 무료/유료/조건부 배송", + "공통 정보 관리: 배송/교환/환불 안내, 판매자 정보", + "장바구니: 추가/수량 변경/삭제, 옵션별 관리", + "체크아웃: 주소 입력, 결제수단 선택, 쿠폰 적용", + "주문 관리: 주문 목록, 상태 변경, 상세 정보, 주문 타임라인", + "주문 완료: 주문 번호, 결제 정보 요약", + "결제 연동 인터페이스 (PaymentInterface)", + "상품 상세 페이지: 이미지 갤러리, 옵션 선택, 수량 입력, 장바구니 담기", + "상품 목록: 필터링 (카테고리/브랜드/가격), 정렬, 페이지네이션, 카드 그리드", + "위시리스트: 찜하기/해제 기능", + "상품 검색: 키워드 검색, 카테고리 필터 연동", + "관리자 레이아웃 (상품, 주문, 카테고리, 브랜드, 쿠폰, 배송정책 관리)", + "사용자 레이아웃 (상품 목록/상세, 장바구니, 체크아웃, 주문완료)", + "권한 시스템 연동", + "다국어 지원 (ko, en)" + ] + } + ] + } + ] + } +} +``` **에러 응답** @@ -499,9 +3144,37 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/modules/sirsoft-ecommerce/dependent-templates HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_목록 응답: `data.data[]` 배열 항목의 필드._ + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "의존 템플릿 정보를 성공적으로 조회했습니다.", + "data": { + "data": [], + "total": 0 + } +} +``` **에러 응답** @@ -528,9 +3201,38 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/modules/sirsoft-ecommerce/license HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| content | string | `프로그램 명칭 : 그누보드7용 이커머스 모듈 (sirsoft-eco…` | 본문 내용 | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "모듈을 성공적으로 가져왔습니다.", + "data": { + "content": "프로그램 명칭 : 그누보드7용 이커머스 모듈 (sirsoft-ecommerce)\n\n저작자 : (주)에스아이알소프트\n\n----- MIT 라이선스 (한국어 번역) --------------------------------------------------------\n\nMIT 라이선스\n\nCopyright (c) 2026 (주)에스아이알소프트\n\n이 소프트웨어와 관련 문서 파일(이하 \"소프트웨어\")의 복사본을 취득하는 모든 사람에게\n소프트웨어를 제한 없이 사용, 복사, 수정, 병합, 출판, 배포, 서브라이선스 허여 및/또는\n판매할 수 있는 권리를 무상으로 부여합니다. 다만, 소프트웨어를 제공받은 사람은 다음\n조건을 따라야 합니다:\n\n위 저작권 고지와 본 허가 고지는 소프트웨어의 모든 복사본 또는 상당 부분에 포함되어야\n합니다.\n\n소프트웨어는 \"있는 그대로\" 제공되며, 명시적이든 묵시적이든 어떠한 종류의 보증도 하지\n않습니다. 여기에는 상품성, 특정 목적에의 적합성 및 비침해에 대한 보증이 포함되나 이에\n국한되지 않습니다. 어떠한 경우에도 저작자 또는 저작권자는 소프트웨어나 소프트웨어의\n사용 또는 기타 거래로 인해 발생하는 계약, 불법행위 또는 기타 청구, 손해 또는 기타\n책임에 대해 책임을 지지 않습니다.\n\n----- MIT License (English Original) --------------------------------------------------------\n\nThe MIT License (MIT)\n\nCopyright (c) 2026 SIRSOFT\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + } +} +``` **에러 응답** @@ -557,10 +3259,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | moduleName | path | string | 예 | — | 대상 module의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/modules/{moduleName} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -586,10 +3301,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | moduleName | path | string | 예 | — | 대상 module의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/modules/{moduleName}/check-modified-layouts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -615,10 +3343,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | moduleName | path | string | 예 | — | 대상 module의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/modules/{moduleName}/install-preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -644,10 +3385,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | moduleName | path | string | 예 | — | 대상 module의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/modules/{moduleName}/uninstall-info HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -678,10 +3432,30 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.perform_update_validation_rules`). +**요청 예시** + +```http +POST /api/admin/modules/{moduleName}/update HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "layout_strategy": "overwrite", + "vendor_mode": "auto", + "force": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -708,13 +3482,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | | path | path | string | 예 | — | 경로 | -| identifier | query | string | 예 | — | 대상 확장/리소스의 식별자 | -| path | query | string | 예 | — | 경로 | + +**요청 예시** + +```http +GET /api/modules/assets/sirsoft-ecommerce/{path}?identifier=example-key HTTP/1.1 +Host: api.example.com +Accept: application/json +``` **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -737,10 +3521,22 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/bundle.css HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** _대표 에러 없음 (공개 조회). 활성 모듈 에셋이 없어도 빈 200 응답을 반환하므로 404 를 내지 않습니다._ @@ -760,10 +3556,22 @@ _대표 에러 없음 (공개 조회). 활성 모듈 에셋이 없어도 빈 200 _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/bundle.js HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** _대표 에러 없음 (공개 조회). 활성 모듈 에셋이 없어도 빈 200 응답을 반환하므로 404 를 내지 않습니다._ @@ -785,9 +3593,38 @@ _대표 에러 없음 (공개 조회). 활성 모듈 에셋이 없어도 빈 200 | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/components.json HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "identifier": "sirsoft-ecommerce", + "version": "1.0.0-beta.5", + "components": { + "basic": [], + "composite": [], + "layout": [] + } +} +``` **에러 응답** @@ -812,9 +3649,8589 @@ _대표 에러 없음 (공개 조회). 활성 모듈 에셋이 없어도 빈 200 | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/editor-spec HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `sirsoft-ecommerce` | 모듈 고유 식별자 (vendor-module 형식) | +| spec | object | `{"$schema":"https:\/\/json-schema.org\/draft\/2020-12\/sc…` | 스펙 정의 객체 (편집기/컴포넌트 선언 스키마 등) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "편집기 스펙을 조회했습니다.", + "data": { + "identifier": "sirsoft-ecommerce", + "spec": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "moduleId": "sirsoft-ecommerce", + "version": "1.0.0", + "description": "레이아웃 편집기 스펙 — 이커머스 모듈 도메인 sampleData/sampleGlobal/states. admin 레이아웃 data_source ID 전수 스캔 기반 도메인 ID 28종(상품·주문·브랜드·쿠폰·배송정책·정산·설정 등) byDataSourceId + 사용자 페이지(템플릿 렌더) byEndpointPattern. 공용 인프라(roles/availableChannels/identityProviders/ecommerceIdentity*/ecommerceNotificationDefinitions)는 admin 템플릿 스펙·코어 프리셋 폴백이 커버.", + "actionRecipes": { + "comment": "이커머스 모듈 소유 친화 액션 레시피. 코어 시드 위에 module 단계로 병합(__source:{kind:module,id:sirsoft-ecommerce}, 편집기 〔이커머스〕 배지). 라벨은 모듈 격리 네임스페이스($t:sirsoft-ecommerce.editor.action.*) — 편집기 t()가 모듈 lang 통짜 ko.json/en.json 의 editor.action 을 sirsoft-ecommerce.editor.action 으로 해석한다(코어/템플릿 editor 와 격리). 결제(PG) 진입은 커머스 도메인이라 코어가 아닌 모듈이 소유한다(provider-agnostic — 핸들러명을 백엔드 응답값으로 받음).", + "requestPgPayment": { + "comment": "결제(PG) 진입 — provider-agnostic. 백엔드 응답이 호출할 결제 진입 핸들러 풀네임(pg_payment_handler)을 데이터 칩으로 연결하면 그 핸들러를 dispatch(특정 PG 하드코딩 없음). build.handler 가 {{paymentHandler}} placeholder 라 matchAction 이 placeholder-aware 로 역매칭한다. onSuccess 컨텍스트(chipContext=response)에서 결제 응답 칩 노출.", + "label": "$t:sirsoft-ecommerce.editor.action.request_pg_payment.label", + "params": [ + { + "key": "paymentHandler", + "label": "$t:sirsoft-ecommerce.editor.action.request_pg_payment.param_handler", + "widget": "data-chip", + "required": true + }, + { + "key": "pgPaymentData", + "label": "$t:sirsoft-ecommerce.editor.action.request_pg_payment.param_data", + "widget": "data-chip", + "required": true + } + ], + "build": { + "handler": "{{paymentHandler}}", + "params": { + "pgPaymentData": "{{pgPaymentData}}" + } + } + } + }, + "actionChipCandidates": { + "comment": "이커머스 도메인 응답 칩 후보 — 코어 기본 응답 칩(response.data/message) 뒤에 병합. 주문 생성 응답이 내려주는 PG 결제 진입 필드(pg_payment_handler/pg_payment_data)를 결제 진입 레시피의 데이터 칩으로 연결할 수 있게 노출한다. 코어는 PG 도메인을 모르고, 본 모듈이 자기 응답 필드를 선언한다(라벨은 모듈 격리 네임스페이스).", + "response": [ + { + "path": "data.pg_payment_handler", + "labelKey": "sirsoft-ecommerce.editor.action_chip.response_pg_handler", + "shape": "scalar" + }, + { + "path": "data.pg_payment_data", + "labelKey": "sirsoft-ecommerce.editor.action_chip.response_pg_data", + "shape": "object" + } + ] + }, + "sampleGlobal": { + "comment": "이커머스 도메인 _global keyspace. 코어 keyspace(currentUser/settings)는 시드하지 않음(충돌 시 코어 우선). 헤더 통화/배송국가 셀렉터(header_currency 슬롯 주입)는 실사이트에서 init_actions 가 채우는 파생 상태(availableCurrencies/availableShippingCountries)와 modules['sirsoft-ecommerce'] 설정을 표시 조건으로 읽는다. 편집기 캔버스는 init_actions 를 실행하지 않으므로 이 키들을 직접 시드해 편집기에서도 셀렉터가 보이게 한다(양 템플릿 공용 — 헤더는 모듈 무지 유지).", + "cart": { + "items": [], + "count": 0, + "total": 0 + }, + "wishlist": { + "items": [], + "count": 0 + }, + "recent_products": [], + "modules": { + "sirsoft-ecommerce": { + "comment": "헤더 통화/배송국가 셀렉터 표시 조건 소스. 실사이트 G7Config(window.__G7_MODULE_CONFIG) 미러 — 편집기 캔버스 전용 시드.", + "language_currency": { + "default_currency": "KRW", + "currencies": [ + { + "code": "KRW", + "symbol": "₩", + "flag": "🇰🇷", + "exchange_rate": 1, + "is_default": true + }, + { + "code": "USD", + "symbol": "$", + "flag": "🇺🇸", + "exchange_rate": 0.00073, + "is_default": false + }, + { + "code": "JPY", + "symbol": "¥", + "flag": "🇯🇵", + "exchange_rate": 0.11, + "is_default": false + } + ] + }, + "shipping": { + "international_shipping_enabled": true, + "default_country": "KR", + "available_countries": [ + { + "code": "KR", + "name": { + "ko": "대한민국", + "en": "South Korea", + "ja": "韓国" + }, + "is_active": true + }, + { + "code": "US", + "name": { + "ko": "미국", + "en": "United States", + "ja": "アメリカ" + }, + "is_active": true + }, + { + "code": "JP", + "name": { + "ko": "일본", + "en": "Japan", + "ja": "日本" + }, + "is_active": true + } + ] + } + } + }, + "defaultCurrency": "KRW", + "preferredCurrency": "KRW", + "preferredShippingCountry": "KR", + "availableCurrencies": [ + { + "code": "KRW", + "symbol": "₩", + "flag": "🇰🇷", + "exchange_rate": 1, + "is_default": true + }, + { + "code": "USD", + "symbol": "$", + "flag": "🇺🇸", + "exchange_rate": 0.00073, + "is_default": false + }, + { + "code": "JPY", + "symbol": "¥", + "flag": "🇯🇵", + "exchange_rate": 0.11, + "is_default": false + } + ], + "availableShippingCountries": [ + { + "code": "KR", + "name": "대한민국" + }, + { + "code": "US", + "name": "미국" + }, + { + "code": "JP", + "name": "일본" + } + ], + "mileageManual": [], + "mileageEdit": [], + "mileageExtend": [] + }, + "sampleData": { + "comment": "실제 모듈 레이아웃 data_source ID(admin: products/categories/orders/order/reviews 등) + 사용자 페이지(템플릿이 렌더, /api/modules/sirsoft-ecommerce/* 호출) byEndpointPattern. 계획서 추정과 실제 ID 차이 반영(데이터소스가 SSoT).", + "byDataSourceId": { + "transactions": { + "data": { + "data": [ + { + "id": 2001, + "user_id": 100, + "currency": "KRW", + "type": "earn", + "type_label": "적립", + "admin_badge_group": null, + "user_display_category": "수동 지급", + "amount": 10000, + "amount_formatted": "10,000", + "remaining_amount": 10000, + "remaining_amount_formatted": "10,000", + "balance_after": 65000, + "order_id": null, + "order_option_id": null, + "order_cancel_id": null, + "source_transaction_id": null, + "granted_by": 1, + "granted_by_name": "관리자", + "granted_by_uuid": "admin-uuid-1", + "user_name": "홍길동", + "user_uuid": "u-1001", + "order_number": null, + "description": "Admin earn", + "memo": "이벤트 보상", + "expires_at": "2027-06-21T00:00:00+09:00", + "expires_at_formatted": "Jun 21, 2027 12:00 AM", + "expires_at_date": "2027-06-21", + "expired_at": null, + "expired_at_formatted": null, + "created_at": "2026-06-20T10:00:00+09:00", + "created_at_formatted": "Jun 20, 2026 10:00 AM", + "created_at_date": "2026-06-20", + "is_earning": true, + "can_edit_expiry": true, + "expired_amount": 0, + "expired_amount_formatted": "0", + "expiry_state": "active", + "abilities": { + "can_manage": true + } + }, + { + "id": 2002, + "user_id": 101, + "currency": "KRW", + "type": "deduct", + "type_label": "차감", + "admin_badge_group": null, + "user_display_category": "수동 차감", + "amount": -5000, + "amount_formatted": "-5,000", + "remaining_amount": 0, + "remaining_amount_formatted": "0", + "balance_after": 45000, + "order_id": null, + "order_option_id": null, + "order_cancel_id": null, + "source_transaction_id": null, + "granted_by": 1, + "granted_by_name": "관리자", + "granted_by_uuid": "admin-uuid-1", + "user_name": "김영희", + "user_uuid": "u-1002", + "order_number": null, + "description": null, + "memo": "잘못된 적립 차감", + "expires_at": null, + "expires_at_formatted": null, + "expires_at_date": null, + "expired_at": null, + "expired_at_formatted": null, + "created_at": "2026-06-19T15:30:00+09:00", + "created_at_formatted": "Jun 19, 2026 3:30 PM", + "created_at_date": "2026-06-19", + "is_earning": false, + "can_edit_expiry": false, + "expired_amount": 0, + "expired_amount_formatted": "0", + "expiry_state": "active", + "abilities": { + "can_manage": true + } + } + ], + "abilities": { + "can_manage": true + }, + "currencies": [ + "KRW" + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 20, + "total": 2, + "from": 1, + "to": 2, + "has_more_pages": false + } + } + }, + "products": { + "data": { + "abilities": { + "can_create": true, + "can_update": true + }, + "data": [ + { + "id": 101, + "name": { + "ko": "베이직 오버핏 코튼 티셔츠" + }, + "name_localized": "베이직 오버핏 코튼 티셔츠", + "product_code": "TS-1001", + "sku": "TS-1001", + "thumbnail_url": "/images/placeholder.png", + "list_price": 29000, + "list_price_formatted": "29,000원", + "selling_price": 23200, + "selling_price_formatted": "23,200원", + "discount_rate": 20, + "multi_currency_list_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 22.31, + "formatted": "$22.31", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 23200, + "formatted": "23,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.85, + "formatted": "$17.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 320, + "safe_stock_quantity": 10, + "is_below_safe_stock": false, + "option_stock_sum": 320, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "sales_status_variant": "success", + "display_status": "visible", + "display_status_label": "노출", + "display_status_variant": "success", + "categories": [ + { + "id": 11, + "name": "상의", + "is_primary": true + } + ], + "primary_category": "상의", + "categories_with_path": [ + { + "id": 11, + "path": [ + { + "id": 1, + "name": "패션의류" + }, + { + "id": 11, + "name": "상의" + } + ], + "path_string": "패션의류 > 상의", + "is_primary": true + } + ], + "brand_name": "코지홈", + "shipping_policy_id": 1, + "shipping_policy_name": "기본 배송정책", + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "has_options": true, + "options_count": 6, + "options": [], + "labels": [ + { + "name": "BEST", + "color": "#ef4444" + } + ], + "review_count": 128, + "rating_avg": 4.7, + "created_at": "2026-05-20 14:30:00", + "updated_at": "2026-05-28 09:15:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 102, + "name": { + "ko": "데일리 와이드 슬랙스" + }, + "name_localized": "데일리 와이드 슬랙스", + "product_code": "PT-2042", + "sku": "PT-2042", + "thumbnail_url": "/images/placeholder.png", + "list_price": 39000, + "list_price_formatted": "39,000원", + "selling_price": 39000, + "selling_price_formatted": "39,000원", + "discount_rate": 0, + "multi_currency_list_price": { + "KRW": { + "price": 39000, + "formatted": "39,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 30, + "formatted": "$30.00", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 39000, + "formatted": "39,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 30, + "formatted": "$30.00", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 85, + "safe_stock_quantity": 10, + "is_below_safe_stock": false, + "option_stock_sum": 85, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "sales_status_variant": "success", + "display_status": "visible", + "display_status_label": "노출", + "display_status_variant": "success", + "categories": [ + { + "id": 11, + "name": "상의", + "is_primary": true + } + ], + "primary_category": "상의", + "categories_with_path": [ + { + "id": 11, + "path": [ + { + "id": 1, + "name": "패션의류" + }, + { + "id": 11, + "name": "상의" + } + ], + "path_string": "패션의류 > 상의", + "is_primary": true + } + ], + "brand_name": "어반핏", + "shipping_policy_id": 1, + "shipping_policy_name": "기본 배송정책", + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "has_options": false, + "options_count": 0, + "options": [], + "labels": [], + "review_count": 42, + "rating_avg": 4.3, + "created_at": "2026-05-20 14:30:00", + "updated_at": "2026-05-28 09:15:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 103, + "name": { + "ko": "구스다운 경량 패딩 점퍼" + }, + "name_localized": "구스다운 경량 패딩 점퍼", + "product_code": "OW-3310", + "sku": "OW-3310", + "thumbnail_url": "/images/placeholder.png", + "list_price": 189000, + "list_price_formatted": "189,000원", + "selling_price": 132300, + "selling_price_formatted": "132,300원", + "discount_rate": 30, + "multi_currency_list_price": { + "KRW": { + "price": 189000, + "formatted": "189,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 145.38, + "formatted": "$145.38", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 132300, + "formatted": "132,300원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 101.77, + "formatted": "$101.77", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 0, + "safe_stock_quantity": 10, + "is_below_safe_stock": true, + "option_stock_sum": 0, + "sales_status": "sold_out", + "sales_status_label": "품절", + "sales_status_variant": "danger", + "display_status": "hidden", + "display_status_label": "미노출", + "display_status_variant": "gray", + "categories": [ + { + "id": 11, + "name": "상의", + "is_primary": true + } + ], + "primary_category": "상의", + "categories_with_path": [ + { + "id": 11, + "path": [ + { + "id": 1, + "name": "패션의류" + }, + { + "id": 11, + "name": "상의" + } + ], + "path_string": "패션의류 > 상의", + "is_primary": true + } + ], + "brand_name": "노스벨리", + "shipping_policy_id": 1, + "shipping_policy_name": "기본 배송정책", + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "has_options": true, + "options_count": 9, + "options": [], + "labels": [ + { + "name": "BEST", + "color": "#ef4444" + } + ], + "review_count": 271, + "rating_avg": 4.9, + "created_at": "2026-05-20 14:30:00", + "updated_at": "2026-05-28 09:15:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + } + ], + "pagination": { + "current_page": 1, + "last_page": 4, + "per_page": 20, + "total": 73 + } + } + }, + "product": { + "data": { + "id": 101, + "name": { + "ko": "베이직 오버핏 코튼 티셔츠", + "en": "Basic Overfit Cotton T-Shirt" + }, + "product_code": "TS-1001", + "sales_product_code": "SP-TS-1001", + "abilities": { + "can_update": true, + "can_delete": true + }, + "images": [ + { + "id": 5001, + "hash": "img5001", + "original_filename": "tshirt_main.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "정면", + "is_thumbnail": true, + "sort_order": 0, + "order": 0, + "width": 1000, + "height": 1000, + "file_size": 184320, + "size": 184320, + "size_formatted": "180 KB", + "mime_type": "image/jpeg", + "is_image": true + }, + { + "id": 5002, + "hash": "img5002", + "original_filename": "tshirt_back.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "후면", + "is_thumbnail": false, + "sort_order": 1, + "order": 1, + "width": 1000, + "height": 1000, + "file_size": 176128, + "size": 176128, + "size_formatted": "172 KB", + "mime_type": "image/jpeg", + "is_image": true + }, + { + "id": 5003, + "hash": "img5003", + "original_filename": "tshirt_detail.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "디테일", + "is_thumbnail": false, + "sort_order": 2, + "order": 2, + "width": 1000, + "height": 1000, + "file_size": 158720, + "size": 158720, + "size_formatted": "155 KB", + "mime_type": "image/jpeg", + "is_image": true + } + ], + "created_at": "2026-05-20 14:30:00", + "updated_at": "2026-05-28 09:15:00" + } + }, + "categories": { + "data": { + "abilities": { + "can_create": true, + "can_update": true + }, + "data": [ + { + "id": 1, + "name": { + "ko": "패션의류" + }, + "localized_name": "패션의류", + "description": null, + "parent_id": null, + "path": "1", + "depth": 0, + "sort_order": 1, + "is_active": true, + "slug": "fashion", + "url": "fashion", + "icon": "folder", + "meta_title": null, + "meta_description": null, + "created_at": "2026-04-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "children": [ + { + "id": 11, + "name": { + "ko": "상의" + }, + "localized_name": "상의", + "description": null, + "parent_id": 1, + "path": "1/11", + "depth": 1, + "sort_order": 11, + "is_active": true, + "slug": "tops", + "url": "tops", + "icon": "folder", + "meta_title": null, + "meta_description": null, + "created_at": "2026-04-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "children": [ + { + "id": 111, + "name": { + "ko": "티셔츠" + }, + "localized_name": "티셔츠", + "description": null, + "parent_id": 11, + "path": "11/111", + "depth": 2, + "sort_order": 111, + "is_active": true, + "slug": "tshirts", + "url": "tshirts", + "icon": "folder", + "meta_title": null, + "meta_description": null, + "created_at": "2026-04-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "children": [] + }, + { + "id": 112, + "name": { + "ko": "셔츠/블라우스" + }, + "localized_name": "셔츠/블라우스", + "description": null, + "parent_id": 11, + "path": "11/112", + "depth": 2, + "sort_order": 112, + "is_active": true, + "slug": "shirts", + "url": "shirts", + "icon": "folder", + "meta_title": null, + "meta_description": null, + "created_at": "2026-04-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "children": [] + } + ] + }, + { + "id": 12, + "name": { + "ko": "하의" + }, + "localized_name": "하의", + "description": null, + "parent_id": 1, + "path": "1/12", + "depth": 1, + "sort_order": 12, + "is_active": true, + "slug": "bottoms", + "url": "bottoms", + "icon": "folder", + "meta_title": null, + "meta_description": null, + "created_at": "2026-04-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "children": [ + { + "id": 121, + "name": { + "ko": "슬랙스" + }, + "localized_name": "슬랙스", + "description": null, + "parent_id": 12, + "path": "12/121", + "depth": 2, + "sort_order": 121, + "is_active": true, + "slug": "slacks", + "url": "slacks", + "icon": "folder", + "meta_title": null, + "meta_description": null, + "created_at": "2026-04-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "children": [] + } + ] + } + ] + }, + { + "id": 2, + "name": { + "ko": "가방/잡화" + }, + "localized_name": "가방/잡화", + "description": null, + "parent_id": null, + "path": "2", + "depth": 0, + "sort_order": 2, + "is_active": true, + "slug": "bags", + "url": "bags", + "icon": "folder", + "meta_title": null, + "meta_description": null, + "created_at": "2026-04-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "children": [ + { + "id": 21, + "name": { + "ko": "백팩" + }, + "localized_name": "백팩", + "description": null, + "parent_id": 2, + "path": "2/21", + "depth": 1, + "sort_order": 21, + "is_active": true, + "slug": "backpack", + "url": "backpack", + "icon": "folder", + "meta_title": null, + "meta_description": null, + "created_at": "2026-04-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "children": [] + } + ] + }, + { + "id": 3, + "name": { + "ko": "아우터" + }, + "localized_name": "아우터", + "description": null, + "parent_id": null, + "path": "3", + "depth": 0, + "sort_order": 3, + "is_active": true, + "slug": "outer", + "url": "outer", + "icon": "folder", + "meta_title": null, + "meta_description": null, + "created_at": "2026-04-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "children": [] + } + ] + } + }, + "orders": { + "data": { + "data": [ + { + "id": 5101, + "no": 1, + "order_number": "20260601-0042", + "order_status": "preparing", + "order_status_label": "배송준비중", + "order_status_variant": "info", + "total_amount": 69500, + "total_amount_formatted": "69,500원", + "total_shipping_amount": 3000, + "total_shipping_amount_formatted": "3,000원", + "total_paid_amount": 69500, + "total_paid_amount_formatted": "69,500원", + "total_unpaid_amount": 0, + "total_unpaid_amount_formatted": "0원", + "ordered_at_formatted": "2026-06-01 19:30", + "order_device_label": "PC", + "is_first_order": false, + "options_count": 2, + "user": { + "uuid": "u-1001", + "name": "홍길동", + "email": "gildong@example.com" + }, + "first_option": { + "product_name": "프리미엄 무선 이어버드 프로", + "product_option_name": "미드나잇 블랙 / 기본", + "product_code": "EARBUDS-PRO", + "quantity": 2, + "thumbnail_url": null + }, + "address": { + "orderer_name": "홍길동", + "recipient_name": "홍길동", + "recipient_country_code": "KR", + "recipient_country_name": "대한민국" + }, + "payment": { + "payment_method": "card", + "payment_method_label": "신용카드" + }, + "shipping": { + "shipping_type": "parcel", + "shipping_type_label": "택배", + "shipping_method_label": "일반배송", + "carrier_name": "CJ대한통운", + "tracking_number": null + } + }, + { + "id": 5102, + "no": 2, + "order_number": "20260530-0118", + "order_status": "shipping", + "order_status_label": "배송중", + "order_status_variant": "primary", + "total_amount": 42000, + "total_amount_formatted": "42,000원", + "total_shipping_amount": 0, + "total_shipping_amount_formatted": "0원", + "total_paid_amount": 42000, + "total_paid_amount_formatted": "42,000원", + "total_unpaid_amount": 0, + "total_unpaid_amount_formatted": "0원", + "ordered_at_formatted": "2026-05-30 13:05", + "order_device_label": "PC", + "is_first_order": false, + "options_count": 1, + "user": { + "uuid": "u-1002", + "name": "김영희", + "email": "younghee@example.com" + }, + "first_option": { + "product_name": "휴대용 블루투스 스피커", + "product_option_name": "기본", + "product_code": "SPEAKER-MINI", + "quantity": 1, + "thumbnail_url": null + }, + "address": { + "orderer_name": "김영희", + "recipient_name": "김영희", + "recipient_country_code": "KR", + "recipient_country_name": "대한민국" + }, + "payment": { + "payment_method": "vbank", + "payment_method_label": "가상계좌" + }, + "shipping": { + "shipping_type": "parcel", + "shipping_type_label": "택배", + "shipping_method_label": "일반배송", + "carrier_name": "CJ대한통운", + "tracking_number": "6123456789020" + } + }, + { + "id": 5103, + "no": 3, + "order_number": "20260528-0096", + "order_status": "payment_pending", + "order_status_label": "입금대기", + "order_status_variant": "warning", + "total_amount": 128000, + "total_amount_formatted": "128,000원", + "total_shipping_amount": 3000, + "total_shipping_amount_formatted": "3,000원", + "total_paid_amount": 0, + "total_paid_amount_formatted": "0원", + "total_unpaid_amount": 128000, + "total_unpaid_amount_formatted": "128,000원", + "ordered_at_formatted": "2026-05-28 21:40", + "order_device_label": "PC", + "is_first_order": true, + "options_count": 1, + "user": { + "uuid": "u-1003", + "name": "John Smith", + "email": "john@example.com" + }, + "first_option": { + "product_name": "스마트 워치 밴드", + "product_option_name": "브라운 / M", + "product_code": "WATCH-BAND", + "quantity": 1, + "thumbnail_url": null + }, + "address": { + "orderer_name": "John Smith", + "recipient_name": "John Smith", + "recipient_country_code": "US", + "recipient_country_name": "United States" + }, + "payment": { + "payment_method": "dbank", + "payment_method_label": "무통장입금" + }, + "shipping": { + "shipping_type": "parcel", + "shipping_type_label": "택배", + "shipping_method_label": "일반배송", + "carrier_name": "CJ대한통운", + "tracking_number": null + } + }, + { + "id": 5104, + "no": 4, + "order_number": "20260525-0071", + "order_status": "cancelled", + "order_status_label": "주문취소", + "order_status_variant": "danger", + "total_amount": 24000, + "total_amount_formatted": "24,000원", + "total_shipping_amount": 3000, + "total_shipping_amount_formatted": "3,000원", + "total_paid_amount": 0, + "total_paid_amount_formatted": "0원", + "total_unpaid_amount": 0, + "total_unpaid_amount_formatted": "0원", + "ordered_at_formatted": "2026-05-25 10:12", + "order_device_label": "PC", + "is_first_order": false, + "options_count": 1, + "user": { + "uuid": "u-1004", + "name": "이철수", + "email": "chulsoo@example.com" + }, + "first_option": { + "product_name": "USB-C 멀티 어댑터", + "product_option_name": "실버", + "product_code": "ADAPTER-USBC", + "quantity": 1, + "thumbnail_url": null + }, + "address": { + "orderer_name": "이철수", + "recipient_name": "이철수", + "recipient_country_code": "KR", + "recipient_country_name": "대한민국" + }, + "payment": { + "payment_method": "card", + "payment_method_label": "신용카드" + }, + "shipping": { + "shipping_type": "parcel", + "shipping_type_label": "택배", + "shipping_method_label": "일반배송", + "carrier_name": "CJ대한통운", + "tracking_number": null + } + } + ], + "statistics": { + "pending_payment": 1, + "payment_complete": 0, + "preparing": 1, + "shipping": 1, + "delivered": 0, + "confirmed": 0, + "cancelled": 1 + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 20, + "total": 4, + "from": 1, + "to": 4, + "has_more_pages": false + }, + "abilities": { + "can_update": true + } + } + }, + "order": { + "data": { + "id": 5101, + "order_number": "20260601-0042", + "order_status": "preparing", + "order_status_label": "배송준비중", + "order_status_variant": "info", + "order_device": "pc", + "order_device_label": "PC", + "is_first_order": false, + "abilities": { + "can_read": true, + "can_update": true, + "can_cancel": true + }, + "subtotal_amount": 76000, + "subtotal_amount_formatted": "76,000원", + "total_discount_amount": 9500, + "total_discount_amount_formatted": "9,500원", + "total_shipping_amount": 3000, + "total_shipping_amount_formatted": "3,000원", + "total_amount": 69500, + "total_amount_formatted": "69,500원", + "total_paid_amount": 69500, + "total_paid_amount_formatted": "69,500원", + "total_product_coupon_discount_amount": 5000, + "total_product_coupon_discount_amount_formatted": "5,000원", + "total_order_coupon_discount_amount": 2500, + "total_order_coupon_discount_amount_formatted": "2,500원", + "total_coupon_discount_amount": 7500, + "total_coupon_discount_amount_formatted": "7,500원", + "total_code_discount_amount": 2000, + "total_code_discount_amount_formatted": "2,000원", + "total_points_used_amount": 1000, + "total_points_used_amount_formatted": "1,000원", + "total_deposit_used_amount": 0, + "total_deposit_used_amount_formatted": "0원", + "total_earned_points_amount": 710, + "total_earned_points_amount_formatted": "710원", + "total_tax_amount": 6318, + "total_tax_amount_formatted": "6,318원", + "total_vat_amount": 6318, + "total_vat_amount_formatted": "6,318원", + "total_list_price": 82000, + "total_list_price_formatted": "82,000원", + "total_quantity": 3, + "item_count": 2, + "mc_subtotal_amount": { + "KRW": { + "price": 76000, + "formatted": "76,000원", + "is_default": true + }, + "USD": { + "price": 55.7, + "formatted": "$55.70", + "is_default": false + } + }, + "mc_total_discount_amount": { + "KRW": { + "price": 9500, + "formatted": "9,500원", + "is_default": true + }, + "USD": { + "price": 6.9, + "formatted": "$6.90", + "is_default": false + } + }, + "mc_total_shipping_amount": { + "KRW": { + "price": 3000, + "formatted": "3,000원", + "is_default": true + }, + "USD": { + "price": 2.2, + "formatted": "$2.20", + "is_default": false + } + }, + "mc_total_amount": { + "KRW": { + "price": 69500, + "formatted": "69,500원", + "is_default": true + }, + "USD": { + "price": 50.7, + "formatted": "$50.70", + "is_default": false + } + }, + "ordered_at_formatted": "2026-06-01 19:30", + "paid_at_formatted": "2026-06-01 19:31", + "delivered_at": null, + "user_id": "u-1001", + "user_login_id": "gildong", + "orderer_name": "홍길동", + "orderer_phone": "010-1234-5678", + "orderer_tel": "02-555-1234", + "orderer_email": "gildong@example.com", + "recipient_name": "홍길동", + "recipient_phone": "010-1234-5678", + "recipient_zipcode": "06234", + "recipient_address": "서울특별시 강남구 테헤란로 123", + "recipient_detail_address": "4층 401호", + "delivery_memo": "부재 시 경비실에 맡겨주세요", + "shipping_address": { + "recipient_name": "홍길동", + "recipient_phone": "010-1234-5678", + "zipcode": "06234", + "address": "서울특별시 강남구 테헤란로 123", + "address_detail": "4층 401호", + "full_address": "서울특별시 강남구 테헤란로 123 4층 401호", + "delivery_memo": "부재 시 경비실에 맡겨주세요" + }, + "options": [ + { + "id": 9101, + "option_status": "preparing", + "option_status_label": "배송준비중", + "option_status_variant": "info", + "product_id": 1, + "product_option_id": 101, + "sku": "EARBUDS-PRO-BLK", + "product_name": "프리미엄 무선 이어버드 프로", + "product_option_name": "미드나잇 블랙 / 기본", + "option_name": "미드나잇 블랙 / 기본", + "quantity": 2, + "shipped_quantity": 0, + "unit_price": 29000, + "unit_price_formatted": "29,000원", + "subtotal_price": 58000, + "subtotal_price_formatted": "58,000원", + "subtotal_discount_amount": 7500, + "subtotal_discount_amount_formatted": "7,500원", + "list_price": 32000, + "list_price_formatted": "32,000원", + "final_amount": 50500, + "final_amount_formatted": "50,500원", + "product_coupon_discount_amount": 5000, + "product_coupon_discount_amount_formatted": "5,000원", + "order_coupon_discount_amount": 2500, + "order_coupon_discount_amount_formatted": "2,500원", + "coupon_discount_amount": 7500, + "coupon_discount_amount_formatted": "7,500원", + "code_discount_amount": 0, + "code_discount_amount_formatted": "0원", + "referral_discount_amount": 0, + "referral_discount_amount_formatted": "0원", + "subtotal_points_used_amount": 500, + "subtotal_points_used_amount_formatted": "500원", + "subtotal_deposit_used_amount": 0, + "subtotal_deposit_used_amount_formatted": "0원", + "subtotal_earned_points_amount": 530, + "subtotal_earned_points_amount_formatted": "530원", + "subtotal_tax_amount": 4818, + "subtotal_tax_amount_formatted": "4,818원", + "subtotal_vat_amount": 4818, + "subtotal_vat_amount_formatted": "4,818원", + "shipping_policy_name": "기본 배송 정책", + "shipping_type_label": "택배", + "shipping_amount": 3000, + "shipping_amount_formatted": "3,000원", + "carrier_name": "CJ대한통운", + "tracking_number": "6123456789012", + "thumbnail_url": null, + "product_snapshot": { + "product_code": "EARBUDS-PRO" + }, + "mc_unit_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true + }, + "USD": { + "price": 21.3, + "formatted": "$21.30", + "is_default": false + } + }, + "mc_subtotal_price": { + "KRW": { + "price": 58000, + "formatted": "58,000원", + "is_default": true + }, + "USD": { + "price": 42.6, + "formatted": "$42.60", + "is_default": false + } + }, + "confirmed_at": null, + "promotions_applied_snapshot": { + "coupons": [ + { + "name": "이어버드 단독 10% 할인", + "total_discount_formatted": "5,000원" + } + ], + "discount_codes": [ + { + "code": "WELCOME2000", + "name": "신규가입 2,000원 할인코드", + "total_discount_formatted": "2,000원" + } + ], + "events": [] + } + }, + { + "id": 9102, + "option_status": "preparing", + "option_status_label": "배송준비중", + "option_status_variant": "info", + "product_id": 2, + "product_option_id": 102, + "sku": "CABLE-15M-WHT", + "product_name": "고속 충전 케이블 1.5m", + "product_option_name": "1.5m", + "option_name": "1.5m", + "quantity": 1, + "shipped_quantity": 0, + "unit_price": 18000, + "unit_price_formatted": "18,000원", + "subtotal_price": 18000, + "subtotal_price_formatted": "18,000원", + "subtotal_discount_amount": 0, + "subtotal_discount_amount_formatted": "0원", + "list_price": 18000, + "list_price_formatted": "18,000원", + "final_amount": 18000, + "final_amount_formatted": "18,000원", + "product_coupon_discount_amount": 0, + "product_coupon_discount_amount_formatted": "0원", + "order_coupon_discount_amount": 0, + "order_coupon_discount_amount_formatted": "0원", + "coupon_discount_amount": 0, + "coupon_discount_amount_formatted": "0원", + "code_discount_amount": 0, + "code_discount_amount_formatted": "0원", + "referral_discount_amount": 0, + "referral_discount_amount_formatted": "0원", + "subtotal_points_used_amount": 0, + "subtotal_points_used_amount_formatted": "0원", + "subtotal_deposit_used_amount": 0, + "subtotal_deposit_used_amount_formatted": "0원", + "subtotal_earned_points_amount": 180, + "subtotal_earned_points_amount_formatted": "180원", + "subtotal_tax_amount": 1500, + "subtotal_tax_amount_formatted": "1,500원", + "subtotal_vat_amount": 1500, + "subtotal_vat_amount_formatted": "1,500원", + "shipping_policy_name": "무료 배송 정책", + "shipping_type_label": "택배", + "shipping_amount": 0, + "shipping_amount_formatted": "0원", + "carrier_name": "CJ대한통운", + "tracking_number": "6123456789013", + "thumbnail_url": null, + "product_snapshot": { + "product_code": "CABLE-15M" + }, + "mc_unit_price": { + "KRW": { + "price": 18000, + "formatted": "18,000원", + "is_default": true + }, + "USD": { + "price": 13.2, + "formatted": "$13.20", + "is_default": false + } + }, + "mc_subtotal_price": { + "KRW": { + "price": 18000, + "formatted": "18,000원", + "is_default": true + }, + "USD": { + "price": 13.2, + "formatted": "$13.20", + "is_default": false + } + }, + "confirmed_at": null, + "promotions_applied_snapshot": { + "coupons": [], + "discount_codes": [], + "events": [] + } + } + ], + "payment": { + "payment_method_label": "신용카드" + }, + "payments": [ + { + "id": 8101, + "payment_status": "paid", + "payment_status_label": "결제완료", + "payment_status_variant": "success", + "payment_method": "card", + "payment_method_label": "신용카드", + "payment_type_label": "일반결제", + "payment_number": "PAY-20260601-0042", + "paid_amount_formatted": "69,500원", + "account_info": null, + "requested_at_formatted": "2026-06-01 19:30", + "paid_at_formatted": "2026-06-01 19:31", + "due_date_formatted": null, + "cancelled_amount": 0, + "cancelled_amount_formatted": "0원", + "refunded_amount": 0, + "refunded_amount_formatted": "0원", + "refund_status_label": null, + "refund_status_variant": null, + "refund_points_amount": 0, + "refund_points_amount_formatted": "0원", + "event_discount_amount": 0, + "event_discount_amount_formatted": "0원", + "referral_discount_amount": 0, + "referral_discount_amount_formatted": "0원" + } + ], + "shippings": [], + "promotions_applied_snapshot": { + "product_promotions": { + "coupons": [ + { + "coupon_id": 5001, + "name": "이어버드 단독 10% 할인", + "target_type": "product_amount", + "total_discount": 5000, + "total_discount_formatted": "5,000원" + } + ], + "discount_codes": [ + { + "code_id": 7001, + "code": "WELCOME2000", + "name": "신규가입 2,000원 할인코드", + "total_discount": 2000, + "total_discount_formatted": "2,000원" + } + ], + "events": [] + }, + "order_promotions": { + "coupons": [ + { + "coupon_id": 6001, + "name": "5만원 이상 5,000원 할인", + "target_type": "order_amount", + "total_discount": 5000, + "total_discount_formatted": "5,000원" + } + ], + "discount_codes": [], + "events": [] + } + }, + "shipping_policy_applied_snapshot": { + "policy_name": "기본 배송 정책", + "shipping_amount": 3000 + } + } + }, + "reviews": { + "data": { + "abilities": { + "can_update": true, + "can_delete": true + }, + "data": [ + { + "id": 9101, + "product_id": 101, + "user": { + "uuid": "u-aaa", + "name": "김민지", + "email": "minji@example.com" + }, + "product": { + "id": 101, + "name": "베이직 오버핏 코튼 티셔츠", + "thumbnail_url": "/images/placeholder.png" + }, + "option_snapshot_label": "블랙 / M", + "rating": 5, + "content": "핏이 정말 예쁘고 원단도 도톰해서 만족스러워요.", + "content_mode": "text", + "status": "visible", + "status_label": "노출", + "status_badge_color": "green", + "image_count": 2, + "has_reply": true, + "has_reply_label": "답변완료", + "has_reply_badge_color": "green", + "reply_content": "소중한 후기 감사합니다!", + "replied_at": "2026-05-27 09:10:00", + "created_at": "2026-05-26 11:20:00", + "updated_at": "2026-05-26 11:20:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 9102, + "product_id": 101, + "user": { + "uuid": "u-bbb", + "name": "이서준", + "email": "seojun@example.com" + }, + "product": { + "id": 101, + "name": "베이직 오버핏 코튼 티셔츠", + "thumbnail_url": "/images/placeholder.png" + }, + "option_snapshot_label": "화이트 / L", + "rating": 4, + "content": "색감 예쁘고 좋아요.", + "content_mode": "text", + "status": "visible", + "status_label": "노출", + "status_badge_color": "green", + "image_count": 0, + "has_reply": false, + "has_reply_label": "미답변", + "has_reply_badge_color": "gray", + "reply_content": null, + "replied_at": null, + "created_at": "2026-05-24 16:42:00", + "updated_at": "2026-05-24 16:42:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 9103, + "product_id": 102, + "user": { + "uuid": "u-ccc", + "name": "박도윤", + "email": "doyun@example.com" + }, + "product": { + "id": 102, + "name": "데일리 와이드 슬랙스", + "thumbnail_url": "/images/placeholder.png" + }, + "option_snapshot_label": "", + "rating": 2, + "content": "기대보다 별로였어요.", + "content_mode": "text", + "status": "hidden", + "status_label": "숨김", + "status_badge_color": "gray", + "image_count": 0, + "has_reply": false, + "has_reply_label": "미답변", + "has_reply_badge_color": "gray", + "reply_content": null, + "replied_at": null, + "created_at": "2026-05-22 10:05:00", + "updated_at": "2026-05-22 10:05:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + } + ], + "meta": { + "current_page": 1, + "last_page": 2, + "per_page": 15, + "total": 21 + } + } + }, + "brands": { + "data": { + "abilities": { + "can_create": true, + "can_update": true + }, + "data": [ + { + "id": 1, + "name": { + "ko": "코지홈" + }, + "localized_name": "코지홈", + "slug": "cozyhome", + "url": "cozyhome", + "website": "https://cozyhome.example.com", + "sort_order": 1, + "is_active": true, + "icon": "tag", + "description": { + "ko": "데일리 베이직 의류 브랜드" + }, + "products_count": 42, + "created_at": "2026-03-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 2, + "name": { + "ko": "어반핏" + }, + "localized_name": "어반핏", + "slug": "urbanfit", + "url": "urbanfit", + "website": null, + "sort_order": 2, + "is_active": true, + "icon": "tag", + "description": { + "ko": "모던 시티 캐주얼" + }, + "products_count": 18, + "created_at": "2026-03-05 10:00:00", + "updated_at": "2026-05-02 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 3, + "name": { + "ko": "노스벨리" + }, + "localized_name": "노스벨리", + "slug": "northvalley", + "url": "northvalley", + "website": "https://northvalley.example.com", + "sort_order": 3, + "is_active": false, + "icon": "tag", + "description": null, + "products_count": 7, + "created_at": "2026-03-10 10:00:00", + "updated_at": "2026-05-03 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + } + ] + } + }, + "carriers": { + "data": [ + { + "id": 1, + "name": "샘플 택배", + "code": "sample", + "enabled": true + }, + { + "id": 2, + "name": "샘플 퀵", + "code": "quick", + "enabled": false + } + ] + }, + "active_carriers": { + "data": [ + { + "id": 1, + "name": "샘플 택배", + "code": "sample" + } + ] + }, + "coupons": { + "data": { + "abilities": { + "can_create": true, + "can_update": true + }, + "data": [ + { + "id": 3001, + "name": { + "ko": "신규회원 10% 할인" + }, + "localized_name": "신규회원 10% 할인", + "code": "WELCOME10", + "target_type": "order_amount", + "target_type_label": "주문금액", + "discount_type": "rate", + "discount_value": 10, + "benefit_formatted": "10% 할인", + "min_order_amount": 20000, + "max_discount_amount": 5000, + "is_active": true, + "issued_count": 1240, + "used_count": 318, + "valid_from": "2026-05-01 00:00:00", + "valid_to": "2026-06-30 23:59:59", + "created_at": "2026-04-25 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 3002, + "name": { + "ko": "5,000원 즉시할인" + }, + "localized_name": "5,000원 즉시할인", + "code": "SAVE5000", + "target_type": "order_amount", + "target_type_label": "주문금액", + "discount_type": "fixed", + "discount_value": 5000, + "benefit_formatted": "5,000원 할인", + "min_order_amount": 30000, + "max_discount_amount": null, + "is_active": true, + "issued_count": 800, + "used_count": 145, + "valid_from": "2026-05-10 00:00:00", + "valid_to": "2026-06-15 23:59:59", + "created_at": "2026-05-08 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 3003, + "name": { + "ko": "무료배송 쿠폰" + }, + "localized_name": "무료배송 쿠폰", + "code": "FREESHIP", + "target_type": "shipping_fee", + "target_type_label": "배송비", + "discount_type": "fixed", + "discount_value": 3000, + "benefit_formatted": "배송비 3,000원 할인", + "min_order_amount": 0, + "max_discount_amount": 3000, + "is_active": false, + "issued_count": 0, + "used_count": 0, + "valid_from": "2026-06-01 00:00:00", + "valid_to": "2026-07-31 23:59:59", + "created_at": "2026-05-20 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + } + ], + "pagination": { + "current_page": 1, + "last_page": 3, + "per_page": 15, + "total": 38 + } + } + }, + "coupon": { + "data": { + "id": 1, + "name": { + "ko": "신규회원 10% 할인", + "en": "New Member 10% Off" + }, + "localized_name": "신규회원 10% 할인", + "description": { + "ko": "신규 가입 회원 대상 주문금액 10% 할인 쿠폰", + "en": "10% off for new members" + }, + "localized_description": "신규 가입 회원 대상 주문금액 10% 할인 쿠폰", + "target_type": "order_amount", + "target_type_label": "주문금액", + "target_type_badge_color": "blue", + "discount_type": "rate", + "discount_type_label": "정률", + "discount_value": 10, + "discount_max_amount": 20000, + "min_order_amount": 30000, + "benefit_formatted": "10% 할인 (최대 20,000원)", + "issue_method": "download", + "issue_method_label": "다운로드", + "issue_method_badge_color": "green", + "issue_condition": "manual", + "issue_condition_label": "수동발급", + "issue_condition_badge_color": "gray", + "issue_status": "active", + "issue_status_label": "발급중", + "issue_status_badge_color": "green", + "total_quantity": 1000, + "issued_count": 342, + "per_user_limit": 1, + "issue_count_formatted": "342 / 1,000", + "valid_type": "days", + "valid_days": 30, + "valid_from": null, + "valid_to": null, + "valid_period_formatted": "발급일로부터 30일", + "issue_from": "2026-05-01T00:00", + "issue_to": "2026-06-30T23:59", + "issue_period_formatted": "2026-05-01 ~ 2026-06-30", + "is_combinable": false, + "target_scope": "all", + "target_scope_label": "제한 없음", + "is_issuable": true, + "created_at": "2026-04-25 10:00", + "updated_at": "2026-05-08 14:20", + "created_by_name": "관리자", + "included_products": [], + "excluded_products": [], + "included_categories": [], + "excluded_categories": [], + "issues_count": 342, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } + }, + "couponIssues": { + "data": { + "data": [ + { + "id": 9001, + "coupon_id": 1, + "user_id": "u-1001", + "user_name": "홍길동", + "coupon_code": "WELCOME-A1B2", + "status": "used", + "status_label": "사용완료", + "status_badge_color": "gray", + "issued_at": "2026-05-02 10:15", + "expired_at": "2026-06-01 23:59", + "used_at": "2026-05-10 14:20", + "order_id": 5101, + "discount_amount": 7000, + "is_expired": false, + "is_usable": false + }, + { + "id": 9002, + "coupon_id": 1, + "user_id": "u-1002", + "user_name": "김영희", + "coupon_code": "WELCOME-C3D4", + "status": "issued", + "status_label": "발급완료", + "status_badge_color": "green", + "issued_at": "2026-05-15 09:40", + "expired_at": "2026-06-14 23:59", + "used_at": null, + "order_id": null, + "discount_amount": 0, + "is_expired": false, + "is_usable": true + }, + { + "id": 9003, + "coupon_id": 1, + "user_id": "u-1003", + "user_name": "이철수", + "coupon_code": "WELCOME-E5F6", + "status": "expired", + "status_label": "기간만료", + "status_badge_color": "red", + "issued_at": "2026-04-01 11:00", + "expired_at": "2026-05-01 23:59", + "used_at": null, + "order_id": null, + "discount_amount": 0, + "is_expired": true, + "is_usable": false + } + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 10, + "total": 3 + } + } + }, + "policy": { + "data": { + "id": 1, + "name": { + "ko": "기본 배송정책", + "en": "Standard Shipping" + }, + "name_localized": "기본 배송정책", + "is_active": true, + "is_default": true, + "sort_order": 0, + "created_at": "2026-05-01 09:00", + "updated_at": "2026-05-20 11:30", + "fee_summary": "3,000원 (50,000원 이상 무료배송)", + "countries_display": [ + { + "country_code": "KR", + "country_name": "대한민국", + "flag": "🇰🇷" + }, + { + "country_code": "US", + "country_name": "미국", + "flag": "🇺🇸" + } + ], + "country_settings": [ + { + "id": 11, + "country_code": "KR", + "shipping_method": "parcel", + "shipping_method_label": "택배", + "custom_shipping_name": null, + "currency_code": "KRW", + "charge_policy": "conditional_free", + "charge_policy_label": "조건부 무료", + "base_fee": 3000, + "free_threshold": 50000, + "ranges": null, + "api_endpoint": null, + "api_request_fields": null, + "api_response_fee_field": null, + "extra_fee_enabled": true, + "extra_fee_settings": [ + { + "region": "제주특별자치도", + "zipcode": "63000-63644", + "fee": 3000 + }, + { + "region": "도서산간", + "zipcode": "", + "fee": 4000 + } + ], + "extra_fee_multiply": false, + "is_active": true + }, + { + "id": 12, + "country_code": "US", + "shipping_method": "express", + "shipping_method_label": "특송", + "custom_shipping_name": null, + "currency_code": "USD", + "charge_policy": "range_amount", + "charge_policy_label": "구간별(금액)", + "base_fee": 0, + "free_threshold": null, + "ranges": [ + { + "min": 0, + "max": 100000, + "fee": 25000 + }, + { + "min": 100000, + "max": null, + "fee": 15000 + } + ], + "api_endpoint": null, + "api_request_fields": null, + "api_response_fee_field": null, + "extra_fee_enabled": false, + "extra_fee_settings": null, + "extra_fee_multiply": false, + "is_active": true + } + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } + }, + "shipping_policies": { + "data": { + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + }, + "data": [ + { + "id": 1, + "name": { + "ko": "기본 배송정책" + }, + "name_localized": "기본 배송정책", + "is_active": true, + "is_default": true, + "sort_order": 1, + "fee_summary": "3,000원 (50,000원 이상 무료배송)", + "countries_display": "🇰🇷 대한민국", + "country_settings": [ + { + "id": 1, + "country_code": "KR", + "country_name": "대한민국", + "shipping_method": "parcel", + "charge_policy": "conditional_free", + "base_fee": 3000, + "base_fee_formatted": "3,000원", + "currency_code": "KRW", + "free_threshold": 50000, + "free_threshold_formatted": "50,000원", + "carrier_label": "CJ대한통운" + } + ], + "created_at": "2026-03-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 2, + "name": { + "ko": "해외 배송정책" + }, + "name_localized": "해외 배송정책", + "is_active": true, + "is_default": false, + "sort_order": 2, + "fee_summary": "국가별 차등 (25,000원~)", + "countries_display": "🇰🇷 🇺🇸 외 3개국", + "country_settings": [ + { + "id": 1, + "country_code": "KR", + "country_name": "대한민국", + "shipping_method": "parcel", + "charge_policy": "conditional_free", + "base_fee": 3000, + "base_fee_formatted": "3,000원", + "currency_code": "KRW", + "free_threshold": 50000, + "free_threshold_formatted": "50,000원", + "carrier_label": "CJ대한통운" + }, + { + "id": 2, + "country_code": "US", + "country_name": "미국", + "shipping_method": "express", + "charge_policy": "paid", + "base_fee": 25000, + "base_fee_formatted": "25,000원", + "currency_code": "KRW", + "free_threshold": null, + "free_threshold_formatted": null, + "carrier_label": "EMS" + } + ], + "created_at": "2026-03-02 10:00:00", + "updated_at": "2026-05-02 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 3, + "name": { + "ko": "무료배송 정책" + }, + "name_localized": "무료배송 정책", + "is_active": false, + "is_default": false, + "sort_order": 3, + "fee_summary": "전 상품 무료배송", + "countries_display": "🇰🇷 대한민국", + "country_settings": [ + { + "id": 1, + "country_code": "KR", + "country_name": "대한민국", + "shipping_method": "parcel", + "charge_policy": "free", + "base_fee": 0, + "base_fee_formatted": "0원", + "currency_code": "KRW", + "free_threshold": 0, + "free_threshold_formatted": "0원", + "carrier_label": "CJ대한통운" + } + ], + "created_at": "2026-03-03 10:00:00", + "updated_at": "2026-05-03 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + } + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 20, + "total": 3 + } + } + }, + "extra_fee_templates": { + "data": { + "data": [ + { + "id": 1, + "description": "제주 도서산간 추가배송비", + "region": "제주특별자치도", + "zipcode": "63000-63644", + "fee": 3000, + "fee_formatted": "3,000원", + "is_active": true + }, + { + "id": 2, + "description": "울릉도/독도 추가배송비", + "region": "경상북도 울릉군", + "zipcode": "40200-40240", + "fee": 5000, + "fee_formatted": "5,000원", + "is_active": true + }, + { + "id": 3, + "description": "기타 도서산간 추가배송비", + "region": "전국 도서산간", + "zipcode": "", + "fee": 4000, + "fee_formatted": "4,000원", + "is_active": false + } + ] + } + }, + "presets": { + "data": [ + { + "id": 1, + "name": "기본 프리셋", + "config": [] + } + ] + }, + "product_labels": { + "data": { + "abilities": { + "can_create": true, + "can_update": true + }, + "data": [ + { + "id": 1, + "name": { + "ko": "BEST" + }, + "color": "#ef4444", + "is_active": true, + "sort_order": 1, + "assignments_count": 12, + "created_at": "2026-03-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 2, + "name": { + "ko": "NEW" + }, + "color": "#3b82f6", + "is_active": true, + "sort_order": 2, + "assignments_count": 8, + "created_at": "2026-03-02 10:00:00", + "updated_at": "2026-05-02 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 3, + "name": { + "ko": "한정특가" + }, + "color": "#f59e0b", + "is_active": false, + "sort_order": 3, + "assignments_count": 0, + "created_at": "2026-03-03 10:00:00", + "updated_at": "2026-05-03 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + } + ] + } + }, + "notice_templates": { + "data": { + "abilities": { + "can_create": true, + "can_update": true + }, + "data": [ + { + "id": 1, + "name": { + "ko": "의류 상품정보제공고시" + }, + "localized_name": "의류 상품정보제공고시", + "category": "clothing", + "fields": [ + { + "key": "material", + "label": { + "ko": "제품 소재" + } + }, + { + "key": "size", + "label": { + "ko": "치수" + } + }, + { + "key": "manufacturer", + "label": { + "ko": "제조자" + } + } + ], + "fields_count": 3, + "is_active": true, + "sort_order": 1, + "icon": "file-alt", + "created_at": "2026-03-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 2, + "name": { + "ko": "가공식품 상품정보제공고시" + }, + "localized_name": "가공식품 상품정보제공고시", + "category": "food", + "fields": [ + { + "key": "name", + "label": { + "ko": "제품명" + } + }, + { + "key": "expiry", + "label": { + "ko": "소비기한" + } + } + ], + "fields_count": 2, + "is_active": true, + "sort_order": 2, + "icon": "file-alt", + "created_at": "2026-03-02 10:00:00", + "updated_at": "2026-05-02 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 3, + "name": { + "ko": "전자제품 상품정보제공고시" + }, + "localized_name": "전자제품 상품정보제공고시", + "category": "electronics", + "fields": [ + { + "key": "model", + "label": { + "ko": "모델명" + } + } + ], + "fields_count": 1, + "is_active": false, + "sort_order": 3, + "icon": "file-alt", + "created_at": "2026-03-03 10:00:00", + "updated_at": "2026-05-03 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + } + ] + } + }, + "templates": { + "data": { + "data": [ + { + "id": 1, + "name": "기본 안내문", + "body_html": "

샘플 상품 안내 템플릿

" + } + ], + "meta": { + "total": 1, + "per_page": 20, + "current_page": 1 + }, + "abilities": { + "can_create": true + } + } + }, + "common_infos": { + "data": { + "abilities": { + "can_create": true, + "can_update": true + }, + "data": [ + { + "id": 1, + "name": { + "ko": "기본 배송/교환/환불 안내" + }, + "localized_name": "기본 배송/교환/환불 안내", + "content": { + "ko": "

주문 후 영업일 기준 1~2일 내 출고됩니다. 단순 변심 교환·반품은 수령 후 7일 이내 가능합니다.

" + }, + "localized_content": "

주문 후 영업일 기준 1~2일 내 출고됩니다. 단순 변심 교환·반품은 수령 후 7일 이내 가능합니다.

", + "content_mode": "html", + "is_default": true, + "is_active": true, + "sort_order": 1, + "icon": "info-circle", + "products_count": 30, + "language_count": 1, + "created_at": "2026-03-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 2, + "name": { + "ko": "식품 보관 안내" + }, + "localized_name": "식품 보관 안내", + "content": { + "ko": "직사광선을 피해 서늘한 곳에 보관하세요." + }, + "localized_content": "직사광선을 피해 서늘한 곳에 보관하세요.", + "content_mode": "text", + "is_default": false, + "is_active": true, + "sort_order": 2, + "icon": "info-circle", + "products_count": 5, + "language_count": 1, + "created_at": "2026-03-02 10:00:00", + "updated_at": "2026-05-02 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 3, + "name": { + "ko": "대형가전 설치 안내" + }, + "localized_name": "대형가전 설치 안내", + "content": { + "ko": "

설치 기사 방문 일정은 별도 안내됩니다.

" + }, + "localized_content": "

설치 기사 방문 일정은 별도 안내됩니다.

", + "content_mode": "html", + "is_default": false, + "is_active": true, + "sort_order": 3, + "icon": "info-circle", + "products_count": 2, + "language_count": 1, + "created_at": "2026-03-03 10:00:00", + "updated_at": "2026-05-03 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + } + ] + } + }, + "commonInfos": { + "data": { + "abilities": { + "can_create": true, + "can_update": true + }, + "data": [ + { + "id": 1, + "name": { + "ko": "기본 배송/교환/환불 안내" + }, + "localized_name": "기본 배송/교환/환불 안내", + "content": { + "ko": "

주문 후 영업일 기준 1~2일 내 출고됩니다. 단순 변심 교환·반품은 수령 후 7일 이내 가능합니다.

" + }, + "localized_content": "

주문 후 영업일 기준 1~2일 내 출고됩니다. 단순 변심 교환·반품은 수령 후 7일 이내 가능합니다.

", + "content_mode": "html", + "is_default": true, + "is_active": true, + "sort_order": 1, + "icon": "info-circle", + "products_count": 30, + "language_count": 1, + "created_at": "2026-03-01 10:00:00", + "updated_at": "2026-05-01 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 2, + "name": { + "ko": "식품 보관 안내" + }, + "localized_name": "식품 보관 안내", + "content": { + "ko": "직사광선을 피해 서늘한 곳에 보관하세요." + }, + "localized_content": "직사광선을 피해 서늘한 곳에 보관하세요.", + "content_mode": "text", + "is_default": false, + "is_active": true, + "sort_order": 2, + "icon": "info-circle", + "products_count": 5, + "language_count": 1, + "created_at": "2026-03-02 10:00:00", + "updated_at": "2026-05-02 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 3, + "name": { + "ko": "대형가전 설치 안내" + }, + "localized_name": "대형가전 설치 안내", + "content": { + "ko": "

설치 기사 방문 일정은 별도 안내됩니다.

" + }, + "localized_content": "

설치 기사 방문 일정은 별도 안내됩니다.

", + "content_mode": "html", + "is_default": false, + "is_active": true, + "sort_order": 3, + "icon": "info-circle", + "products_count": 2, + "language_count": 1, + "created_at": "2026-03-03 10:00:00", + "updated_at": "2026-05-03 10:00:00", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + } + ] + } + }, + "order_logs": { + "data": { + "data": [ + { + "id": 3001, + "created_at": "2026-06-03 09:12", + "localized_description": "주문 상태를 결제완료에서 배송준비중으로 변경했습니다", + "user": { + "uuid": "admin-1", + "name": "관리자" + }, + "changes": [ + { + "field": "order_status", + "label": "주문상태", + "old": "payment_complete", + "old_label": "결제완료", + "new": "preparing", + "new_label": "배송준비중" + } + ], + "bulk_changes": [] + }, + { + "id": 3002, + "created_at": "2026-06-01 19:31", + "localized_description": "결제가 완료되었습니다", + "user": null, + "changes": [ + { + "field": "payment_status", + "label": "결제상태", + "old": "pending", + "old_label": "결제대기", + "new": "paid", + "new_label": "결제완료" + } + ], + "bulk_changes": [] + }, + { + "id": 3003, + "created_at": "2026-06-01 19:30", + "localized_description": "관리자 메모를 수정했습니다", + "user": { + "uuid": "admin-1", + "name": "관리자" + }, + "changes": [ + { + "field": "admin_memo", + "label": "관리자 메모", + "old": "", + "old_label": "(없음)", + "new": "VIP 고객 — 빠른 배송 요청", + "new_label": "VIP 고객 — 빠른 배송 요청" + }, + { + "field": "delivery_memo", + "label": "배송 메모", + "old": "", + "old_label": "(없음)", + "new": "부재 시 경비실", + "new_label": "부재 시 경비실" + } + ], + "bulk_changes": [] + } + ], + "meta": { + "current_page": 1, + "last_page": 1, + "per_page": 20, + "total": 3 + } + } + }, + "activity_logs": { + "data": { + "data": [ + { + "id": 1, + "log_type": "admin", + "log_type_label": "관리자", + "loggable_type": "Product", + "loggable_type_display": "상품", + "loggable_id": 101, + "action": "product.updated", + "action_label": "상품 수정", + "localized_description": "'베이직 오버핏 코튼 티셔츠' 상품을 수정했습니다.", + "description_key": "product.updated", + "properties": null, + "changes": [ + { + "field": "selling_price", + "label": "판매가", + "old": 29000, + "new": 23200, + "old_label": "29,000원", + "new_label": "23,200원" + }, + { + "field": "sales_status", + "label": "판매상태", + "old": "sold_out", + "new": "on_sale", + "old_label": "품절", + "new_label": "판매중" + } + ], + "bulk_changes": null, + "has_changes": true, + "actor_name": "관리자", + "user": { + "uuid": "admin-uuid-1", + "name": "관리자", + "email": "admin@example.com" + }, + "ip_address": "127.0.0.1", + "created_at": "2026-05-28 09:15:00", + "is_owner": false + }, + { + "id": 2, + "log_type": "admin", + "log_type_label": "관리자", + "loggable_type": "Product", + "loggable_type_display": "상품", + "loggable_id": 101, + "action": "product.created", + "action_label": "상품 등록", + "localized_description": "'베이직 오버핏 코튼 티셔츠' 상품을 등록했습니다.", + "description_key": "product.created", + "properties": null, + "changes": null, + "bulk_changes": null, + "has_changes": false, + "actor_name": "운영자", + "user": { + "uuid": "admin-uuid-2", + "name": "운영자", + "email": "manager@example.com" + }, + "ip_address": "127.0.0.1", + "created_at": "2026-05-20 14:30:00", + "is_owner": false + }, + { + "id": 3, + "log_type": "admin", + "log_type_label": "관리자", + "loggable_type": "Product", + "loggable_type_display": "상품", + "loggable_id": 101, + "action": "product.stock_adjusted", + "action_label": "재고 조정", + "localized_description": "재고를 조정했습니다.", + "description_key": "product.stock_adjusted", + "properties": null, + "changes": [ + { + "field": "stock_quantity", + "label": "재고수량", + "old": 200, + "new": 320, + "old_label": "200", + "new_label": "320" + } + ], + "bulk_changes": null, + "has_changes": true, + "actor_name": "시스템", + "user": { + "name": "시스템" + }, + "ip_address": null, + "created_at": "2026-05-25 03:00:00", + "is_owner": false + } + ], + "meta": { + "current_page": 1, + "last_page": 1, + "per_page": 10, + "total": 3 + } + } + }, + "settings": { + "data": { + "general": { + "shop_name": "샘플 쇼핑몰", + "currency": "KRW" + }, + "shipping": { + "default_fee": 3000, + "free_threshold": 50000 + }, + "inquiry": { + "board_slug": "product-inquiry" + }, + "abilities": { + "can_update": true + } + } + }, + "ecommerce_settings": { + "data": { + "basic_info": { + "shop_name": "", + "route_path": "shop", + "no_route": false, + "company_name": "", + "business_number": "", + "ceo_name": "", + "business_type": "", + "business_category": "", + "zipcode": "", + "base_address": "", + "detail_address": "", + "phone": "", + "fax": "", + "email": "", + "privacy_officer": "", + "privacy_officer_email": "", + "mail_order_number": "", + "telecom_number": "" + }, + "language_currency": { + "default_language": "ko", + "default_currency": "KRW", + "currencies": [ + { + "code": "KRW", + "name": { + "ko": "KRW (원)", + "en": "KRW (Won)", + "ja": "KRW (ウォン)" + }, + "exchange_rate": null, + "rounding_unit": "1", + "rounding_method": "floor", + "decimal_places": 0, + "is_default": true + }, + { + "code": "USD", + "name": { + "ko": "USD (달러)", + "en": "USD (Dollar)", + "ja": "USD (ドル)" + }, + "exchange_rate": 0.85, + "rounding_unit": "0.01", + "rounding_method": "round", + "decimal_places": 2, + "is_default": false + }, + { + "code": "JPY", + "name": { + "ko": "JPY (엔)", + "en": "JPY (Yen)", + "ja": "JPY (円)" + }, + "exchange_rate": 115, + "rounding_unit": "1", + "rounding_method": "floor", + "decimal_places": 0, + "is_default": false + }, + { + "code": "CNY", + "name": { + "ko": "CNY (위안)", + "en": "CNY (Yuan)", + "ja": "CNY (人民元)" + }, + "exchange_rate": 5.8, + "rounding_unit": "0.01", + "rounding_method": "round", + "decimal_places": 2, + "is_default": false + }, + { + "code": "EUR", + "name": { + "ko": "EUR (유로)", + "en": "EUR (Euro)", + "ja": "EUR (ユーロ)" + }, + "exchange_rate": 0.78, + "rounding_unit": "0.01", + "rounding_method": "round", + "decimal_places": 2, + "is_default": false + } + ] + }, + "order_settings": { + "default_pg_provider": null, + "payment_methods": [ + { + "id": "card", + "pg_provider": null, + "sort_order": 1, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "신용카드", + "en": "Credit Card", + "ja": "クレジットカード" + }, + "_cached_description": { + "ko": "신용카드로 안전하게 결제", + "en": "Pay securely with credit card", + "ja": "クレジットカードで安全に決済" + }, + "_cached_icon": "credit-card", + "_cached_source": "builtin" + }, + { + "id": "vbank", + "pg_provider": null, + "sort_order": 2, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "가상계좌", + "en": "Virtual Account", + "ja": "バーチャル口座" + }, + "_cached_description": { + "ko": "가상계좌로 입금", + "en": "Pay via virtual account", + "ja": "バーチャル口座に入金" + }, + "_cached_icon": "money-check", + "_cached_source": "builtin" + }, + { + "id": "dbank", + "pg_provider": null, + "sort_order": 3, + "is_active": true, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "_cached_name": { + "ko": "무통장입금", + "en": "Bank Transfer", + "ja": "銀行振込" + }, + "_cached_description": { + "ko": "지정 계좌로 직접 입금", + "en": "Direct bank transfer", + "ja": "指定口座に直接入金" + }, + "_cached_icon": "building-columns", + "_cached_source": "builtin" + }, + { + "id": "bank", + "pg_provider": null, + "sort_order": 4, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "계좌이체", + "en": "Account Transfer", + "ja": "口座振替" + }, + "_cached_description": { + "ko": "실시간 계좌이체", + "en": "Real-time bank transfer", + "ja": "リアルタイム口座振替" + }, + "_cached_icon": "building-columns", + "_cached_source": "builtin" + }, + { + "id": "phone", + "pg_provider": null, + "sort_order": 5, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "휴대폰결제", + "en": "Mobile Payment", + "ja": "携帯電話決済" + }, + "_cached_description": { + "ko": "휴대폰 소액결제", + "en": "Mobile phone payment", + "ja": "携帯電話小額決済" + }, + "_cached_icon": "mobile-screen-button", + "_cached_source": "builtin" + }, + { + "id": "kginicis_samsung_pay", + "pg_provider": null, + "sort_order": 6, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "삼성페이 (KG이니시스)", + "en": "Samsung Pay (KG Inicis)" + }, + "_cached_description": { + "ko": "삼성페이로 결제 — KG 이니시스를 통해 처리", + "en": "Pay with Samsung Pay via KG Inicis" + }, + "_cached_icon": "mobile-screen-button", + "_cached_source": "plugin:sirsoft-pay_kginicis" + }, + { + "id": "kginicis_naverpay", + "pg_provider": null, + "sort_order": 7, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "네이버페이 (KG이니시스)", + "en": "Naver Pay (KG Inicis)" + }, + "_cached_description": { + "ko": "네이버페이로 결제 — KG 이니시스를 통해 처리", + "en": "Pay with Naver Pay via KG Inicis" + }, + "_cached_icon": "wallet", + "_cached_source": "plugin:sirsoft-pay_kginicis" + }, + { + "id": "kginicis_lpay", + "pg_provider": null, + "sort_order": 8, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "L.pay (KG이니시스)", + "en": "L.pay (KG Inicis)" + }, + "_cached_description": { + "ko": "L.pay 로 결제 — KG 이니시스를 통해 처리", + "en": "Pay with L.pay via KG Inicis" + }, + "_cached_icon": "mobile-screen-button", + "_cached_source": "plugin:sirsoft-pay_kginicis" + }, + { + "id": "kginicis_kakaopay", + "pg_provider": null, + "sort_order": 9, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "카카오페이 (KG이니시스)", + "en": "Kakao Pay (KG Inicis)" + }, + "_cached_description": { + "ko": "카카오페이로 결제 — KG 이니시스를 통해 처리", + "en": "Pay with Kakao Pay via KG Inicis" + }, + "_cached_icon": "mobile-screen-button", + "_cached_source": "plugin:sirsoft-pay_kginicis" + }, + { + "id": "kginicis_japan_paypay", + "pg_provider": null, + "sort_order": 10, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "PayPay (일본 KG이니시스)", + "en": "PayPay (KG Inicis Japan)" + }, + "_cached_description": { + "ko": "일본 엔(JPY) 주문을 KG 이니시스 CBT PayPay로 결제", + "en": "Pay JPY orders with KG Inicis CBT PayPay" + }, + "_cached_icon": "wallet", + "_cached_source": "plugin:sirsoft-pay_kginicis" + }, + { + "id": "point", + "pg_provider": null, + "sort_order": 10, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "_cached_name": { + "ko": "포인트결제", + "en": "Points", + "ja": "ポイント決済" + }, + "_cached_description": { + "ko": "적립 포인트로 결제", + "en": "Pay with points", + "ja": "積立ポイントで決済" + }, + "_cached_icon": "coins", + "_cached_source": "builtin" + }, + { + "id": "kginicis_japan_cvs", + "pg_provider": null, + "sort_order": 11, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "일본 편의점결제 (KG이니시스)", + "en": "Japan Convenience Store (KG Inicis)" + }, + "_cached_description": { + "ko": "일본 엔(JPY) 주문을 KG 이니시스 CBT 편의점 결제로 접수", + "en": "Accept JPY orders with KG Inicis CBT convenience store payment" + }, + "_cached_icon": "store", + "_cached_source": "plugin:sirsoft-pay_kginicis" + }, + { + "id": "deposit", + "pg_provider": null, + "sort_order": 11, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "_cached_name": { + "ko": "예치금결제", + "en": "Store Credit", + "ja": "預金決済" + }, + "_cached_description": { + "ko": "예치금으로 결제", + "en": "Pay with store credit", + "ja": "預金で決済" + }, + "_cached_icon": "wallet", + "_cached_source": "builtin" + }, + { + "id": "free", + "pg_provider": null, + "sort_order": 12, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "_cached_name": { + "ko": "무료", + "en": "Free", + "ja": "無料" + }, + "_cached_description": { + "ko": "결제 없이 주문 완료", + "en": "Order without payment", + "ja": "決済なしで注文完了" + }, + "_cached_icon": "gift", + "_cached_source": "builtin" + } + ], + "banks": [ + { + "code": "004", + "name": { + "ko": "국민은행", + "en": "Kookmin Bank" + } + }, + { + "code": "088", + "name": { + "ko": "신한은행", + "en": "Shinhan Bank" + } + }, + { + "code": "020", + "name": { + "ko": "우리은행", + "en": "Woori Bank" + } + }, + { + "code": "081", + "name": { + "ko": "하나은행", + "en": "Hana Bank" + } + }, + { + "code": "003", + "name": { + "ko": "IBK기업은행", + "en": "IBK Industrial Bank" + } + }, + { + "code": "011", + "name": { + "ko": "NH농협은행", + "en": "NH Nonghyup Bank" + } + }, + { + "code": "071", + "name": { + "ko": "우체국", + "en": "Korea Post" + } + }, + { + "code": "031", + "name": { + "ko": "DGB대구은행", + "en": "DGB Daegu Bank" + } + }, + { + "code": "032", + "name": { + "ko": "BNK부산은행", + "en": "BNK Busan Bank" + } + }, + { + "code": "034", + "name": { + "ko": "광주은행", + "en": "Kwangju Bank" + } + }, + { + "code": "035", + "name": { + "ko": "제주은행", + "en": "Jeju Bank" + } + }, + { + "code": "037", + "name": { + "ko": "전북은행", + "en": "Jeonbuk Bank" + } + }, + { + "code": "039", + "name": { + "ko": "BNK경남은행", + "en": "BNK Kyongnam Bank" + } + }, + { + "code": "045", + "name": { + "ko": "새마을금고", + "en": "MG Community Credit Cooperatives" + } + }, + { + "code": "048", + "name": { + "ko": "신협", + "en": "KFCC" + } + }, + { + "code": "090", + "name": { + "ko": "카카오뱅크", + "en": "Kakao Bank" + } + }, + { + "code": "092", + "name": { + "ko": "토스뱅크", + "en": "Toss Bank" + } + } + ], + "bank_accounts": [ + { + "bank_code": "004", + "account_number": "", + "account_holder": "", + "is_active": false, + "is_default": false + } + ], + "auto_cancel_expired": true, + "auto_cancel_days": 3, + "vbank_due_days": 3, + "dbank_due_days": 7, + "cart_expiry_days": 30, + "stock_restore_on_cancel": true, + "cancellable_statuses": [ + "payment_complete" + ], + "confirmable_statuses": [ + "shipping", + "delivered" + ] + }, + "shipping": { + "default_country": "KR", + "available_countries": [ + { + "code": "KR", + "name": { + "ko": "대한민국", + "en": "South Korea", + "ja": "大韓民国" + }, + "is_active": true + }, + { + "code": "US", + "name": { + "ko": "미국", + "en": "United States", + "ja": "アメリカ" + }, + "is_active": false + }, + { + "code": "JP", + "name": { + "ko": "일본", + "en": "Japan", + "ja": "日本" + }, + "is_active": false + }, + { + "code": "CN", + "name": { + "ko": "중국", + "en": "China", + "ja": "中国" + }, + "is_active": false + }, + { + "code": "SG", + "name": { + "ko": "싱가포르", + "en": "Singapore", + "ja": "シンガポール" + }, + "is_active": false + }, + { + "code": "HK", + "name": { + "ko": "홍콩", + "en": "Hong Kong", + "ja": "香港" + }, + "is_active": false + }, + { + "code": "TW", + "name": { + "ko": "대만", + "en": "Taiwan", + "ja": "台湾" + }, + "is_active": false + }, + { + "code": "VN", + "name": { + "ko": "베트남", + "en": "Vietnam", + "ja": "ベトナム" + }, + "is_active": false + }, + { + "code": "TH", + "name": { + "ko": "태국", + "en": "Thailand", + "ja": "タイ" + }, + "is_active": false + }, + { + "code": "MY", + "name": { + "ko": "말레이시아", + "en": "Malaysia", + "ja": "マレーシア" + }, + "is_active": false + } + ], + "international_shipping_enabled": false, + "free_shipping_threshold": 50000, + "free_shipping_enabled": true, + "address_validation_enabled": false, + "address_api_provider": "kakao", + "carriers": [ + { + "id": 1, + "code": "cj", + "name": { + "ko": "CJ대한통운", + "en": "CJ Logistics", + "ja": "CJ大韓通運" + }, + "type": "domestic", + "tracking_url": "https://trace.cjlogistics.com/next/tracking.html?wblNo={tracking_number}", + "is_active": true, + "sort_order": 1 + }, + { + "id": 2, + "code": "hanjin", + "name": { + "ko": "한진택배", + "en": "Hanjin Express", + "ja": "韓進택배" + }, + "type": "domestic", + "tracking_url": "https://www.hanjin.com/kor/CMS/DeliveryMgr/WaybillResult.do?wblnb={tracking_number}", + "is_active": true, + "sort_order": 2 + }, + { + "id": 3, + "code": "lotte", + "name": { + "ko": "롯데택배", + "en": "Lotte Global Logistics", + "ja": "ロッテ택배" + }, + "type": "domestic", + "tracking_url": "https://www.lotteglogis.com/home/reservation/tracking/link498?InvNo={tracking_number}", + "is_active": true, + "sort_order": 3 + }, + { + "id": 4, + "code": "logen", + "name": { + "ko": "로젠택배", + "en": "Logen Logistics", + "ja": "ロジェン택배" + }, + "type": "domestic", + "tracking_url": "https://www.ilogen.com/web/personal/trace/{tracking_number}", + "is_active": true, + "sort_order": 4 + }, + { + "id": 5, + "code": "ups", + "name": { + "ko": "UPS", + "en": "UPS", + "ja": "UPS" + }, + "type": "international", + "tracking_url": "https://www.ups.com/track?tracknum={tracking_number}", + "is_active": true, + "sort_order": 5 + }, + { + "id": 6, + "code": "ems", + "name": { + "ko": "EMS", + "en": "EMS", + "ja": "EMS" + }, + "type": "international", + "tracking_url": "https://service.epost.go.kr/trace.RetrieveEmsRi498.postal?POST_CODE={tracking_number}", + "is_active": true, + "sort_order": 6 + }, + { + "id": 7, + "code": "dhl", + "name": { + "ko": "DHL", + "en": "DHL", + "ja": "DHL" + }, + "type": "international", + "tracking_url": "https://www.dhl.com/kr-ko/home/tracking/tracking-express.html?submit=1&tracking-id={tracking_number}", + "is_active": true, + "sort_order": 7 + }, + { + "id": 8, + "code": "fedex", + "name": { + "ko": "FedEx", + "en": "FedEx", + "ja": "FedEx" + }, + "type": "international", + "tracking_url": "https://www.fedex.com/fedextrack/?tracknumbers={tracking_number}", + "is_active": true, + "sort_order": 8 + }, + { + "id": 9, + "code": "sf", + "name": { + "ko": "SF Express", + "en": "SF Express", + "ja": "SF Express" + }, + "type": "international", + "tracking_url": null, + "is_active": true, + "sort_order": 9 + }, + { + "id": 10, + "code": "yamato", + "name": { + "ko": "야마토운수", + "en": "Yamato Transport", + "ja": "ヤマト運輸" + }, + "type": "international", + "tracking_url": null, + "is_active": true, + "sort_order": 10 + }, + { + "id": 11, + "code": "sagawa", + "name": { + "ko": "사가와익스프레스", + "en": "Sagawa Express", + "ja": "佐川急便" + }, + "type": "international", + "tracking_url": null, + "is_active": true, + "sort_order": 11 + }, + { + "id": 12, + "code": "other", + "name": { + "ko": "기타", + "en": "Other", + "ja": "その他" + }, + "type": "domestic", + "tracking_url": null, + "is_active": true, + "sort_order": 99 + } + ], + "types": [ + { + "id": 1, + "code": "parcel", + "name": { + "ko": "택배", + "en": "Parcel", + "ja": "宅配便" + }, + "category": "domestic", + "is_active": true, + "sort_order": 1 + }, + { + "id": 2, + "code": "direct", + "name": { + "ko": "직접배송", + "en": "Direct Delivery", + "ja": "直送" + }, + "category": "domestic", + "is_active": true, + "sort_order": 2 + }, + { + "id": 3, + "code": "quick", + "name": { + "ko": "퀵서비스", + "en": "Quick Service", + "ja": "クイックサービス" + }, + "category": "domestic", + "is_active": true, + "sort_order": 3 + }, + { + "id": 4, + "code": "freight", + "name": { + "ko": "화물배송", + "en": "Freight", + "ja": "貨物配送" + }, + "category": "domestic", + "is_active": true, + "sort_order": 4 + }, + { + "id": 5, + "code": "pickup", + "name": { + "ko": "매장수령", + "en": "Store Pickup", + "ja": "店舗受取" + }, + "category": "domestic", + "is_active": true, + "sort_order": 5 + }, + { + "id": 6, + "code": "express", + "name": { + "ko": "국내특급", + "en": "Express", + "ja": "国内特急" + }, + "category": "domestic", + "is_active": false, + "sort_order": 6 + }, + { + "id": 7, + "code": "international_ems", + "name": { + "ko": "국제EMS", + "en": "International EMS", + "ja": "国際EMS" + }, + "category": "international", + "is_active": false, + "sort_order": 7 + }, + { + "id": 8, + "code": "international_standard", + "name": { + "ko": "국제일반", + "en": "International Standard", + "ja": "国際通常" + }, + "category": "international", + "is_active": false, + "sort_order": 8 + }, + { + "id": 9, + "code": "cvs", + "name": { + "ko": "편의점택배", + "en": "Convenience Store", + "ja": "コンビニ宅配便" + }, + "category": "other", + "is_active": false, + "sort_order": 9 + }, + { + "id": 10, + "code": "digital", + "name": { + "ko": "디지털상품", + "en": "Digital", + "ja": "デジタル商品" + }, + "category": "other", + "is_active": false, + "sort_order": 10 + }, + { + "id": 11, + "code": "custom", + "name": { + "ko": "직접입력", + "en": "Custom", + "ja": "直接入力" + }, + "category": "domestic", + "is_active": true, + "sort_order": 99 + } + ] + }, + "seo": { + "meta_category_title": "{commerce_name} - {category_name}", + "meta_category_description": "", + "meta_search_title": "{commerce_name} - {keyword_name}", + "meta_search_description": "", + "meta_product_title": "{commerce_name} - {product_name}", + "meta_product_description": "", + "meta_shop_index_title": "{commerce_name}", + "meta_shop_index_description": "", + "seo_category": true, + "seo_search_result": true, + "seo_product_detail": true, + "seo_shop_index": true + }, + "review_settings": { + "write_deadline_days": 90, + "max_images": 5, + "max_image_size_mb": 10 + }, + "inquiry": { + "board_slug": null + }, + "notifications": { + "channels": [ + { + "id": "mail", + "is_active": true, + "sort_order": 1 + }, + { + "id": "database", + "is_active": true, + "sort_order": 2 + } + ] + }, + "claim": { + "refund_reasons": [ + { + "id": 1, + "type": "refund", + "code": "order_mistake", + "name": { + "ko": "주문 실수", + "en": "Order Mistake", + "ja": "注文ミス" + }, + "localized_name": "주문 실수", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 0 + }, + { + "id": 2, + "type": "refund", + "code": "changed_mind", + "name": { + "ko": "단순 변심", + "en": "Changed Mind", + "ja": "単純な心変わり" + }, + "localized_name": "단순 변심", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 1 + }, + { + "id": 3, + "type": "refund", + "code": "reorder_other", + "name": { + "ko": "다른 상품으로 재주문", + "en": "Reorder with Different Product", + "ja": "別の商品で再注文" + }, + "localized_name": "다른 상품으로 재주문", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 2 + }, + { + "id": 4, + "type": "refund", + "code": "delayed_delivery", + "name": { + "ko": "배송 지연", + "en": "Delayed Delivery", + "ja": "配送遅延" + }, + "localized_name": "배송 지연", + "fault_type": "seller", + "is_user_selectable": true, + "is_active": true, + "sort_order": 3 + }, + { + "id": 5, + "type": "refund", + "code": "product_info_different", + "name": { + "ko": "상품 정보 상이", + "en": "Product Info Different", + "ja": "商品情報の相違" + }, + "localized_name": "상품 정보 상이", + "fault_type": "seller", + "is_user_selectable": true, + "is_active": true, + "sort_order": 4 + }, + { + "id": 6, + "type": "refund", + "code": "admin_cancel", + "name": { + "ko": "관리자 취소", + "en": "Admin Cancel", + "ja": "管理者キャンセル" + }, + "localized_name": "관리자 취소", + "fault_type": "seller", + "is_user_selectable": false, + "is_active": true, + "sort_order": 5 + }, + { + "id": 7, + "type": "refund", + "code": "etc", + "name": { + "ko": "기타", + "en": "Etc", + "ja": "その他" + }, + "localized_name": "기타", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 6 + } + ] + }, + "available_pg_providers": [ + { + "id": "kginicis", + "name": "KG 이니시스", + "icon": "credit-card", + "supported_methods": [ + "card", + "bank_transfer", + "virtual_account", + "mobile" + ] + } + ], + "abilities": { + "can_update": true + } + } + }, + "seoStats": { + "data": { + "count": 0, + "size_bytes": 0, + "size_formatted": "0 B" + } + }, + "copy_source": { + "data": { + "id": 102, + "name": { + "ko": "데일리 와이드 슬랙스", + "en": "Daily Wide Slacks" + }, + "product_code": "PT-2042", + "selling_price": 39000, + "thumbnail_url": "/images/placeholder.png", + "images": [ + { + "id": 5101, + "hash": "img5101", + "original_filename": "slacks_main.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "정면", + "is_thumbnail": true, + "sort_order": 0, + "order": 0, + "width": 1000, + "height": 1000, + "file_size": 172000, + "size": 172000, + "size_formatted": "168 KB", + "mime_type": "image/jpeg", + "is_image": true + }, + { + "id": 5102, + "hash": "img5102", + "original_filename": "slacks_detail.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "디테일", + "is_thumbnail": false, + "sort_order": 1, + "order": 1, + "width": 1000, + "height": 1000, + "file_size": 165000, + "size": 165000, + "size_formatted": "161 KB", + "mime_type": "image/jpeg", + "is_image": true + } + ] + } + }, + "refundReasons": { + "data": [ + { + "id": 1, + "reason": "단순 변심", + "code": "change_mind" + }, + { + "id": 2, + "reason": "상품 불량", + "code": "defective" + } + ] + }, + "availableChannels": { + "data": { + "channels": [ + { + "id": "mail", + "name": "이메일", + "icon": "fas fa-envelope", + "source": "core", + "source_label": "코어" + }, + { + "id": "sms", + "name": "SMS", + "icon": "fas fa-comment-sms", + "source": "core", + "source_label": "코어" + }, + { + "id": "push", + "name": "푸시", + "icon": "fas fa-bell", + "source": "core", + "source_label": "코어" + } + ] + } + }, + "identityProviders": { + "data": [ + { + "id": "kginicis", + "label": "KG이니시스" + }, + { + "id": "nice", + "label": "나이스" + } + ] + }, + "ecommerceIdentityPurposes": { + "data": [ + { + "id": "signup", + "label": "회원가입", + "source_identifier": "sirsoft-ecommerce", + "description": "회원가입 시 본인인증", + "allowed_channels": [ + "mobile" + ] + }, + { + "id": "sensitive_action", + "label": "민감 작업", + "source_identifier": "core", + "description": "민감 작업 시 본인인증", + "allowed_channels": [ + "mobile", + "ipin" + ] + } + ] + }, + "ecommerceIdentityPolicies": { + "data": { + "data": [ + { + "id": 1, + "key": "shop.signup", + "scope": "route", + "target": "/shop/signup", + "purpose": "signup", + "provider_id": "kginicis", + "grace_minutes": 0, + "enabled": true, + "source_type": "admin", + "abilities": { + "can_update": true, + "can_delete": true + } + } + ], + "meta": { + "total": 1, + "current_page": 1, + "last_page": 1, + "per_page": 20 + }, + "abilities": { + "can_create": true + } + } + }, + "ecommerceNotificationDefinitions": { + "data": { + "data": [ + { + "id": 1, + "type": "order.paid", + "is_active": true, + "is_default": true, + "variables": [ + { + "key": "order_no" + } + ], + "templates": [ + { + "id": 11, + "channel": "mail", + "is_active": true, + "is_default": true, + "subject": { + "ko": "결제 완료 안내", + "en": "Payment completed" + }, + "body": { + "ko": "

결제가 완료되었습니다.

", + "en": "

Payment completed.

" + }, + "recipients": [ + { + "type": "trigger_user" + } + ] + } + ] + } + ], + "pagination": { + "total": 1, + "current_page": 1, + "last_page": 1 + }, + "abilities": { + "can_update": true + } + } + }, + "roles": { + "data": { + "data": [ + { + "id": 1, + "identifier": "super_admin", + "name": "최고 관리자", + "display_name": "최고 관리자", + "description": "전체 권한", + "is_system": true + }, + { + "id": 2, + "identifier": "manager", + "name": "운영자", + "display_name": "운영자", + "description": "운영 권한", + "is_system": false + }, + { + "id": 3, + "identifier": "member", + "name": "일반 회원", + "display_name": "일반 회원", + "description": "기본 회원", + "is_system": true + } + ] + } + }, + "category": { + "data": { + "breadcrumb": [ + "샘플" + ], + "children": [ + "샘플" + ], + "description": "

샘플 설명입니다.

", + "description_localized": "샘플 description_localized", + "images": [ + "샘플" + ], + "name": "샘플 name", + "name_localized": "샘플 name_localized" + } + }, + "qna": { + "data": { + "items": [ + "샘플" + ], + "meta": { + "abilities": { + "can_delete": true, + "can_update": true + }, + "board_settings": "샘플", + "current_page": 3, + "last_page": 3, + "total": 10000 + } + } + }, + "productDownloadableCoupons": { + "data": { + "data": "샘플" + } + }, + "cartItems": { + "data": { + "calculation": { + "items": [ + "샘플" + ], + "summary": { + "base_shipping_total": 10000, + "code_discount": 10000, + "discount_formatted": "10,000원", + "extra_shipping_total": 10000, + "final_amount_formatted": "10,000원", + "mileage_formatted": "10,000원", + "multi_currency": "샘플", + "order_coupon_discount": 10000, + "payment_amount_formatted": "10,000원", + "points_earning": 10000, + "product_coupon_discount": 10000, + "shipping_discount": 10000, + "shipping_fee_formatted": "10,000원", + "subtotal_formatted": "10,000원", + "tax_free_amount": 10000, + "taxable_amount": 10000, + "total_discount": 10000, + "total_shipping": 10000 + } + }, + "item_count": 3, + "items": [ + "샘플" + ] + } + }, + "checkoutData": { + "data": { + "available_coupons": [ + "샘플" + ], + "calculation": { + "items": [ + "샘플" + ], + "promotions": { + "order_promotions": { + "coupons": [ + "샘플" + ] + }, + "product_promotions": { + "coupons": [ + "샘플" + ] + } + }, + "summary": { + "base_shipping_total": 10000, + "code_discount": 10000, + "code_discount_formatted": "10,000원", + "extra_shipping_total": 10000, + "final_amount": 10000, + "final_amount_formatted": "10,000원", + "mileage_formatted": "10,000원", + "multi_currency": "샘플", + "order_coupon_discount": 10000, + "order_coupon_discount_formatted": "10,000원", + "payment_amount_formatted": "10,000원", + "points_earning": 10000, + "points_used": 10000, + "points_used_formatted": "10,000원", + "product_coupon_discount": 10000, + "shipping_discount": 10000, + "shipping_discount_formatted": "10,000원", + "subtotal_formatted": "10,000원", + "tax_free_amount": 10000, + "taxable_amount": 10000, + "total_discount": 10000, + "total_discount_formatted": "10,000원", + "total_shipping": 10000, + "total_shipping_formatted": "10,000원" + } + }, + "items": [ + "샘플" + ], + "mileage": { + "balance": 10000, + "balance_formatted": "10,000원", + "max_usable": "샘플", + "max_usable_formatted": "10,000원" + }, + "temp_order_id": 1 + } + }, + "userAddresses": { + "data": { + "addresses": { + "data": "샘플" + } + } + }, + "paymentSettings": { + "data": { + "order_settings": { + "bank_accounts": [ + "샘플" + ], + "dbank_due_days": 3, + "payment_methods": "샘플", + "vbank_due_days": 3 + } + } + }, + "orderData": { + "data": { + "mc_subtotal_amount": 10000, + "mc_total_amount": 10000, + "mc_total_discount_amount": 10000, + "mc_total_shipping_amount": 10000, + "options": [ + "샘플" + ], + "order_number": 3, + "payment": { + "dbank_account": "샘플", + "dbank_holder": "샘플 dbank_holder", + "dbank_name": "샘플 dbank_name", + "deposit_due_at_formatted": "10,000원", + "depositor_name": "샘플 depositor_name", + "payment_method": "card", + "vbank_due_at": "2026-05-24 09:32:38", + "vbank_holder": "샘플 vbank_holder", + "vbank_name": "샘플 vbank_name", + "vbank_number": 3 + }, + "shipping_address": { + "address": "샘플 address", + "address_detail": "샘플 address_detail", + "city": "샘플", + "recipient_country_code": "KR", + "recipient_name": "샘플 recipient_name", + "recipient_phone": "010-1234-5678", + "state": "sample", + "zipcode": "sample" + }, + "subtotal_amount_formatted": "10,000원", + "total_amount_formatted": "10,000원", + "total_discount_amount": 10000, + "total_discount_amount_formatted": "10,000원", + "total_shipping_amount": 10000, + "total_shipping_amount_formatted": "10,000원" + } + }, + "wishlist": { + "data": { + "data": [ + "샘플" + ], + "pagination": { + "current_page": 3, + "last_page": 3, + "total": 10000 + } + } + }, + "addresses": { + "data": { + "addresses": { + "data": "샘플" + } + } + }, + "myInquiries": { + "data": { + "items": [ + "샘플" + ], + "meta": { + "board_settings": "샘플", + "current_page": 3, + "last_page": 3, + "total": 10000 + } + } + }, + "board_list_for_inquiry": { + "data": { + "data": [ + { + "id": 1, + "name": "공지사항", + "slug": "notice", + "is_active": true, + "type": "basic", + "description": "공지사항 게시판입니다.", + "per_page": 20, + "per_page_mobile": 15, + "order_by": "created_at", + "order_direction": "DESC", + "categories": [], + "show_view_count": true, + "secret_mode": "{MASKED}", + "use_comment": true, + "use_reply": true, + "max_reply_depth": 5, + "use_report": false, + "comment_order": "ASC", + "max_comment_depth": 10, + "min_title_length": 2, + "max_title_length": 200, + "min_content_length": 2, + "max_content_length": 10000, + "min_comment_length": 2, + "max_comment_length": 1000, + "blocked_keywords": [], + "use_file_upload": false, + "max_file_size": 10, + "max_file_count": 5, + "allowed_extensions": [ + "jpg", + "jpeg", + "png", + "gif", + "webp", + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "ppt", + "pptx", + "hwp", + "txt", + "zip" + ], + "new_display_hours": 24, + "board_managers": [ + { + "uuid": "00000000-0000-4000-8000-000000000001", + "name": "관리자", + "email": "admin@example.com" + } + ], + "board_steps": [], + "board_manager_ids": [ + "00000000-0000-4000-8000-000000000001" + ], + "board_step_ids": [], + "notify_author": true, + "notify_admin_on_post": true, + "created_at": "2026-05-26 02:41:23", + "updated_at": "2026-05-26 02:41:23", + "permissions": null, + "category_post_counts": null, + "posts_count": 0, + "user_abilities": null, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 20, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + }, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } + }, + "notifications": { + "channels": [ + { + "key": "email", + "label": "이메일", + "enabled": true + }, + { + "key": "push", + "label": "푸시", + "enabled": true + } + ], + "data": { + "data": [ + { + "id": 1, + "type": "comment", + "title": "샘플 알림", + "message": "샘플 알림 내용입니다.", + "is_read": false, + "created_at": "2026-05-24 09:32:38", + "link": "/" + } + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "total": 1, + "has_more_pages": false + }, + "unread_count": 2 + } + }, + "mileage_balance": { + "data": { + "mileage": { + "available": 50000, + "pending": 10000, + "total_earned": 150000, + "expiring_soon": 5000, + "enabled": true + } + } + }, + "mileage_history": { + "data": { + "data": [ + { + "id": 1001, + "user_id": 100, + "currency": "KRW", + "type": "earn", + "type_label": "적립", + "admin_badge_group": null, + "user_display_category": "주문 완료", + "amount": 5000, + "amount_formatted": "5,000", + "remaining_amount": 5000, + "remaining_amount_formatted": "5,000", + "balance_after": 55000, + "order_id": null, + "order_option_id": null, + "order_cancel_id": null, + "source_transaction_id": null, + "granted_by": null, + "granted_by_name": null, + "granted_by_uuid": null, + "user_name": "홍길동", + "user_uuid": "u-1001", + "order_number": "20260601-0042", + "description": "Order reward", + "memo": null, + "expires_at": "2027-06-01T00:00:00+09:00", + "expires_at_formatted": "Jun 1, 2027 12:00 AM", + "expires_at_date": "2027-06-01", + "expired_at": null, + "expired_at_formatted": null, + "created_at": "2026-06-01T14:30:00+09:00", + "created_at_formatted": "Jun 1, 2026 2:30 PM", + "created_at_date": "2026-06-01", + "is_earning": true, + "can_edit_expiry": true, + "expired_amount": 0, + "expired_amount_formatted": "0", + "expiry_state": "active", + "abilities": { + "can_manage": false + } + }, + { + "id": 1002, + "user_id": 100, + "currency": "KRW", + "type": "use", + "type_label": "사용", + "admin_badge_group": null, + "user_display_category": "주문", + "amount": -3000, + "amount_formatted": "-3,000", + "remaining_amount": 0, + "remaining_amount_formatted": "0", + "balance_after": 52000, + "order_id": null, + "order_option_id": null, + "order_cancel_id": null, + "source_transaction_id": null, + "granted_by": null, + "granted_by_name": null, + "granted_by_uuid": null, + "user_name": "홍길동", + "user_uuid": "u-1001", + "order_number": "20260530-0118", + "description": null, + "memo": null, + "expires_at": null, + "expires_at_formatted": null, + "expires_at_date": null, + "expired_at": null, + "expired_at_formatted": null, + "created_at": "2026-05-30T13:05:00+09:00", + "created_at_formatted": "May 30, 2026 1:05 PM", + "created_at_date": "2026-05-30", + "is_earning": false, + "can_edit_expiry": false, + "expired_amount": 0, + "expired_amount_formatted": "0", + "expiry_state": "active", + "abilities": { + "can_manage": false + } + } + ], + "abilities": { + "can_manage": false + }, + "currencies": [ + "KRW" + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 20, + "total": 2, + "from": 1, + "to": 2, + "has_more_pages": false + } + } + }, + "reviewSettings": { + "data": { + "review_settings": { + "max_images": 5, + "max_image_size_mb": 10, + "write_deadline_days": 30 + } + } + } + }, + "byEndpointPattern": { + "/api/modules/sirsoft-ecommerce/products*": { + "data": { + "data": [ + { + "id": 1, + "name": "샘플 상품 1", + "price": 29000, + "sale_price": 24000, + "thumbnail": null, + "stock_status": "in_stock", + "rating": 4.5 + }, + { + "id": 2, + "name": "샘플 상품 2", + "price": 15000, + "sale_price": null, + "thumbnail": null, + "stock_status": "in_stock", + "rating": 4 + }, + { + "id": 3, + "name": "샘플 상품 3 (품절)", + "price": 42000, + "sale_price": null, + "thumbnail": null, + "stock_status": "out_of_stock", + "rating": 3.5 + } + ], + "meta": { + "total": 3, + "per_page": 20, + "current_page": 1 + } + } + }, + "/api/modules/sirsoft-ecommerce/cart*": { + "data": { + "items": [ + { + "product_id": 1, + "name": "샘플 상품 1", + "thumbnail": null, + "price": 24000, + "quantity": 2, + "options": "블랙" + }, + { + "product_id": 2, + "name": "샘플 상품 2", + "thumbnail": null, + "price": 15000, + "quantity": 1, + "options": null + } + ], + "total": 63000, + "discount": 0, + "shipping_fee": 3000 + } + }, + "/api/modules/sirsoft-ecommerce/checkout*": { + "data": { + "items": [ + { + "product_id": 1, + "name": "샘플 상품 1", + "price": 24000, + "quantity": 2 + } + ], + "addresses": [ + { + "id": 1, + "recipient": "샘플 수령인", + "address": "서울시 샘플구 샘플로 1", + "is_default": true + } + ], + "payment_methods": [ + { + "code": "card", + "name": "신용카드" + }, + { + "code": "vbank", + "name": "무통장입금" + } + ], + "coupons": [], + "points": 1250 + } + }, + "/api/modules/sirsoft-ecommerce/wishlist*": { + "data": { + "data": [ + { + "id": 1, + "name": "샘플 찜 상품 1", + "price": 29000, + "thumbnail": null, + "stock_status": "in_stock" + }, + { + "id": 2, + "name": "샘플 찜 상품 2", + "price": 15000, + "thumbnail": null, + "stock_status": "in_stock" + } + ], + "meta": { + "total": 2, + "per_page": 20, + "current_page": 1 + } + } + }, + "/api/modules/sirsoft-ecommerce/user/orders*": { + "data": { + "data": [ + { + "id": 1, + "order_no": "20260529-0001", + "created_at": "2026-05-29", + "status": "paid", + "total": 48000, + "first_item_name": "샘플 상품 1", + "item_count": 2 + } + ], + "meta": { + "total": 1, + "per_page": 20, + "current_page": 1 + } + } + }, + "/api/modules/sirsoft-ecommerce/user/addresses*": { + "data": [ + { + "id": 1, + "recipient": "샘플 수령인", + "phone": "010-0000-0000", + "zip": "00000", + "address": "서울시 샘플구 샘플로 1", + "detail": "101동 101호", + "is_default": true, + "label": "집" + } + ] + }, + "/api/modules/sirsoft-ecommerce/user/inquiries*": { + "data": { + "data": [ + { + "id": 1, + "product": { + "id": 1, + "name": "샘플 상품 1" + }, + "title": "배송 문의", + "body": "언제 배송되나요?", + "answer": "내일 발송 예정입니다.", + "created_at": "2026-05-25", + "status": "answered" + } + ], + "meta": { + "total": 1, + "per_page": 20, + "current_page": 1 + } + } + } + } + }, + "states": { + "comment": "이커머스 도메인 변종. 사용자 shop/* 라우트(템플릿 sirsoft-basic 소속이나 코어 concat 으로 일원화)와 관리자 라우트(*/admin/ecommerce/*) 변종을 모두 포함한다. shop/* scope.match 는 편집기가 동적 path 를 'shop' 으로 평가한 selectedRoute.path 와 일치한다(Chrome MCP 실측).", + "groups": [ + { + "comment": "shop/show — product.data.sales_status 값(on_sale/sold_out/suspended/coming_soon)으로 판매 상태 배지·버튼을 if 분기한다(본문 line 388/406). 재고는 stock 으로 분기.", + "scope": { + "kind": "route", + "match": "/shop/products/:id" + }, + "items": [ + { + "id": "in_stock", + "label": "$t:editor.state.in_stock", + "default": true + }, + { + "id": "out_of_stock", + "label": "$t:editor.state.out_of_stock", + "sampleDataOverrides": { + "byEndpointPattern": { + "/api/modules/sirsoft-ecommerce/products/*": { + "data": { + "id": 101, + "name": { + "ko": "베이직 오버핏 코튼 티셔츠", + "en": "Basic Overfit Cotton T-Shirt" + }, + "name_localized": "베이직 오버핏 코튼 티셔츠", + "product_code": "TS-1001", + "sku": "TS-1001-BLK", + "categories": [ + { + "id": 1, + "name": { + "ko": "패션의류", + "en": "Fashion" + }, + "name_localized": "패션의류", + "breadcrumb": "패션의류", + "path": "1", + "is_primary": false + }, + { + "id": 11, + "name": { + "ko": "상의", + "en": "Tops" + }, + "name_localized": "상의", + "breadcrumb": "패션의류 > 상의", + "path": "1/11", + "is_primary": true + } + ], + "category_name": "상의", + "list_price": 29000, + "list_price_formatted": "29,000원", + "selling_price": 23200, + "selling_price_formatted": "23,200원", + "discount_rate": 20, + "multi_currency_list_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 22.31, + "formatted": "$22.31", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 23200, + "formatted": "23,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.85, + "formatted": "$17.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 0, + "min_purchase_qty": 1, + "max_purchase_qty": 10, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "brand_name": "코지홈", + "labels": [ + { + "name": "BEST", + "color": "#ef4444" + }, + { + "name": "한정특가", + "color": "#f59e0b" + } + ], + "additional_options": [ + { + "id": 9001, + "name": "이니셜 자수 추가", + "is_required": false + }, + { + "id": 9002, + "name": "선물 포장", + "is_required": false + } + ], + "shipping_policy_id": 1, + "free_shipping": false, + "shipping_fee_formatted": "3,000원 (50,000원 이상 무료)", + "shipping_policy": { + "name": "기본 배송정책", + "charge_policy": "conditional_free", + "charge_policy_label": "조건부 무료", + "base_fee": 3000, + "base_fee_formatted": "3,000원", + "free_threshold": 50000, + "free_threshold_formatted": "50,000원", + "fee_summary": "3,000원 (50,000원 이상 무료배송)" + }, + "short_description": "사계절 입기 좋은 오버핏 기본 티셔츠", + "short_description_localized": "사계절 입기 좋은 오버핏 기본 티셔츠", + "description": { + "ko": "

부드러운 30수 코튼 소재로 제작된 데일리 오버핏 티셔츠입니다.

" + }, + "description_localized": "

제품 특징

부드러운 30수 코튼 100% 소재로 제작되어 사계절 내내 편안하게 착용할 수 있습니다. 오버핏 실루엣으로 체형 커버에 탁월하며, 어떤 하의와도 자연스럽게 매치됩니다.

  • 소재: 코튼 100%
  • 핏: 오버핏
  • 제조국: 대한민국

\"상세

", + "description_mode": "html", + "images": [ + { + "id": 5001, + "hash": "img5001", + "original_filename": "tshirt_main.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "정면", + "alt_text_current": "베이직 오버핏 티셔츠 정면", + "is_thumbnail": true, + "sort_order": 0, + "order": 0, + "width": 1000, + "height": 1000, + "file_size": 184320, + "size": 184320, + "size_formatted": "180 KB", + "mime_type": "image/jpeg", + "is_image": true + }, + { + "id": 5002, + "hash": "img5002", + "original_filename": "tshirt_back.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "후면", + "alt_text_current": "베이직 오버핏 티셔츠 후면", + "is_thumbnail": false, + "sort_order": 1, + "order": 1, + "width": 1000, + "height": 1000, + "file_size": 176128, + "size": 176128, + "size_formatted": "172 KB", + "mime_type": "image/jpeg", + "is_image": true + }, + { + "id": 5003, + "hash": "img5003", + "original_filename": "tshirt_detail.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "디테일", + "alt_text_current": "소매 디테일 컷", + "is_thumbnail": false, + "sort_order": 2, + "order": 2, + "width": 1000, + "height": 1000, + "file_size": 158720, + "size": 158720, + "size_formatted": "155 KB", + "mime_type": "image/jpeg", + "is_image": true + } + ], + "thumbnail_url": "/images/placeholder.png", + "meta_title": "베이직 오버핏 코튼 티셔츠 | 코지홈", + "meta_description": "사계절 입기 좋은 오버핏 기본 티셔츠", + "meta_keywords": [ + "티셔츠", + "오버핏", + "코튼" + ], + "has_options": true, + "option_groups": [ + { + "name": { + "ko": "색상", + "en": "Color" + }, + "name_localized": "색상", + "values": [ + { + "ko": "블랙", + "en": "Black" + }, + { + "ko": "화이트", + "en": "White" + }, + { + "ko": "네이비", + "en": "Navy" + } + ], + "values_localized": [ + "블랙", + "화이트", + "네이비" + ] + }, + { + "name": { + "ko": "사이즈", + "en": "Size" + }, + "name_localized": "사이즈", + "values": [ + { + "ko": "S", + "en": "S" + }, + { + "ko": "M", + "en": "M" + }, + { + "ko": "L", + "en": "L" + } + ], + "values_localized": [ + "S", + "M", + "L" + ] + } + ], + "options": [ + { + "id": 7001, + "option_code": "OPT-BLK-S", + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "블랙" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "S" + } + } + ], + "option_values_localized": [ + "블랙", + "S" + ], + "option_name": { + "ko": "블랙 / S" + }, + "option_name_localized": "블랙 / S", + "price_adjustment": 0, + "price_adjustment_formatted": "+0원", + "price_adjustment_type": "increase", + "list_price": 29000, + "list_price_formatted": "29,000원", + "selling_price": 23200, + "selling_price_formatted": "23,200원", + "final_price": 23200, + "final_price_formatted": "23,200원", + "multi_currency_list_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 22.31, + "formatted": "$22.31", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 23200, + "formatted": "23,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.85, + "formatted": "$17.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 0, + "safe_stock_quantity": 10, + "is_below_safe_stock": true, + "is_default": true, + "is_active": true, + "sku": "TS-1001-BLK-S", + "sort_order": 0 + }, + { + "id": 7002, + "option_code": "OPT-BLK-M", + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "블랙" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "M" + } + } + ], + "option_values_localized": [ + "블랙", + "M" + ], + "option_name": { + "ko": "블랙 / M" + }, + "option_name_localized": "블랙 / M", + "price_adjustment": 0, + "price_adjustment_formatted": "+0원", + "price_adjustment_type": "increase", + "list_price": 29000, + "list_price_formatted": "29,000원", + "selling_price": 23200, + "selling_price_formatted": "23,200원", + "final_price": 23200, + "final_price_formatted": "23,200원", + "multi_currency_list_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 22.31, + "formatted": "$22.31", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 23200, + "formatted": "23,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.85, + "formatted": "$17.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 0, + "safe_stock_quantity": 10, + "is_below_safe_stock": true, + "is_default": false, + "is_active": true, + "sku": "TS-1001-BLK-M", + "sort_order": 1 + }, + { + "id": 7003, + "option_code": "OPT-WHT-L", + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "화이트" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "L" + } + } + ], + "option_values_localized": [ + "화이트", + "L" + ], + "option_name": { + "ko": "화이트 / L" + }, + "option_name_localized": "화이트 / L", + "price_adjustment": 2000, + "price_adjustment_formatted": "+2,000원", + "price_adjustment_type": "increase", + "list_price": 31000, + "list_price_formatted": "31,000원", + "selling_price": 25200, + "selling_price_formatted": "25,200원", + "final_price": 25200, + "final_price_formatted": "25,200원", + "multi_currency_list_price": { + "KRW": { + "price": 31000, + "formatted": "31,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 23.85, + "formatted": "$23.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 25200, + "formatted": "25,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 19.38, + "formatted": "$19.38", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 0, + "safe_stock_quantity": 10, + "is_below_safe_stock": true, + "is_default": false, + "is_active": true, + "sku": "TS-1001-WHT-L", + "sort_order": 2 + } + ], + "notice": { + "template_name": "의류 상품정보제공고시", + "values": [ + { + "label": "제품 소재", + "value": "면 100%" + }, + { + "label": "색상", + "value": "블랙, 화이트, 네이비" + }, + { + "label": "치수", + "value": "S, M, L (상세 사이즈표 참고)" + }, + { + "label": "제조자", + "value": "코지홈" + }, + { + "label": "제조국", + "value": "대한민국" + }, + { + "label": "취급 시 주의사항", + "value": "찬물 단독 손세탁 권장" + }, + { + "label": "품질보증기준", + "value": "관련 법 및 소비자분쟁해결기준에 따름" + }, + { + "label": "A/S 책임자와 전화번호", + "value": "코지홈 고객센터 1588-0000" + } + ] + }, + "common_info": { + "name": "배송/교환/환불 안내", + "content": "

주문 후 영업일 기준 1~2일 내 출고됩니다. 단순 변심 교환·반품은 수령 후 7일 이내 가능하며, 왕복 배송비가 부과됩니다.

", + "content_mode": "html" + }, + "is_wishlisted": false + } + } + } + } + }, + { + "id": "sold_out", + "label": "$t:editor.state.sold_out", + "sampleDataOverrides": { + "byEndpointPattern": { + "/api/modules/sirsoft-ecommerce/products/*": { + "data": { + "id": 101, + "name": { + "ko": "베이직 오버핏 코튼 티셔츠", + "en": "Basic Overfit Cotton T-Shirt" + }, + "name_localized": "베이직 오버핏 코튼 티셔츠", + "product_code": "TS-1001", + "sku": "TS-1001-BLK", + "categories": [ + { + "id": 1, + "name": { + "ko": "패션의류", + "en": "Fashion" + }, + "name_localized": "패션의류", + "breadcrumb": "패션의류", + "path": "1", + "is_primary": false + }, + { + "id": 11, + "name": { + "ko": "상의", + "en": "Tops" + }, + "name_localized": "상의", + "breadcrumb": "패션의류 > 상의", + "path": "1/11", + "is_primary": true + } + ], + "category_name": "상의", + "list_price": 29000, + "list_price_formatted": "29,000원", + "selling_price": 23200, + "selling_price_formatted": "23,200원", + "discount_rate": 20, + "multi_currency_list_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 22.31, + "formatted": "$22.31", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 23200, + "formatted": "23,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.85, + "formatted": "$17.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 0, + "min_purchase_qty": 1, + "max_purchase_qty": 10, + "sales_status": "sold_out", + "sales_status_label": "품절", + "brand_name": "코지홈", + "labels": [ + { + "name": "BEST", + "color": "#ef4444" + }, + { + "name": "한정특가", + "color": "#f59e0b" + } + ], + "additional_options": [ + { + "id": 9001, + "name": "이니셜 자수 추가", + "is_required": false + }, + { + "id": 9002, + "name": "선물 포장", + "is_required": false + } + ], + "shipping_policy_id": 1, + "free_shipping": false, + "shipping_fee_formatted": "3,000원 (50,000원 이상 무료)", + "shipping_policy": { + "name": "기본 배송정책", + "charge_policy": "conditional_free", + "charge_policy_label": "조건부 무료", + "base_fee": 3000, + "base_fee_formatted": "3,000원", + "free_threshold": 50000, + "free_threshold_formatted": "50,000원", + "fee_summary": "3,000원 (50,000원 이상 무료배송)" + }, + "short_description": "사계절 입기 좋은 오버핏 기본 티셔츠", + "short_description_localized": "사계절 입기 좋은 오버핏 기본 티셔츠", + "description": { + "ko": "

부드러운 30수 코튼 소재로 제작된 데일리 오버핏 티셔츠입니다.

" + }, + "description_localized": "

제품 특징

부드러운 30수 코튼 100% 소재로 제작되어 사계절 내내 편안하게 착용할 수 있습니다. 오버핏 실루엣으로 체형 커버에 탁월하며, 어떤 하의와도 자연스럽게 매치됩니다.

  • 소재: 코튼 100%
  • 핏: 오버핏
  • 제조국: 대한민국

\"상세

", + "description_mode": "html", + "images": [ + { + "id": 5001, + "hash": "img5001", + "original_filename": "tshirt_main.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "정면", + "alt_text_current": "베이직 오버핏 티셔츠 정면", + "is_thumbnail": true, + "sort_order": 0, + "order": 0, + "width": 1000, + "height": 1000, + "file_size": 184320, + "size": 184320, + "size_formatted": "180 KB", + "mime_type": "image/jpeg", + "is_image": true + }, + { + "id": 5002, + "hash": "img5002", + "original_filename": "tshirt_back.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "후면", + "alt_text_current": "베이직 오버핏 티셔츠 후면", + "is_thumbnail": false, + "sort_order": 1, + "order": 1, + "width": 1000, + "height": 1000, + "file_size": 176128, + "size": 176128, + "size_formatted": "172 KB", + "mime_type": "image/jpeg", + "is_image": true + }, + { + "id": 5003, + "hash": "img5003", + "original_filename": "tshirt_detail.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "디테일", + "alt_text_current": "소매 디테일 컷", + "is_thumbnail": false, + "sort_order": 2, + "order": 2, + "width": 1000, + "height": 1000, + "file_size": 158720, + "size": 158720, + "size_formatted": "155 KB", + "mime_type": "image/jpeg", + "is_image": true + } + ], + "thumbnail_url": "/images/placeholder.png", + "meta_title": "베이직 오버핏 코튼 티셔츠 | 코지홈", + "meta_description": "사계절 입기 좋은 오버핏 기본 티셔츠", + "meta_keywords": [ + "티셔츠", + "오버핏", + "코튼" + ], + "has_options": true, + "option_groups": [ + { + "name": { + "ko": "색상", + "en": "Color" + }, + "name_localized": "색상", + "values": [ + { + "ko": "블랙", + "en": "Black" + }, + { + "ko": "화이트", + "en": "White" + }, + { + "ko": "네이비", + "en": "Navy" + } + ], + "values_localized": [ + "블랙", + "화이트", + "네이비" + ] + }, + { + "name": { + "ko": "사이즈", + "en": "Size" + }, + "name_localized": "사이즈", + "values": [ + { + "ko": "S", + "en": "S" + }, + { + "ko": "M", + "en": "M" + }, + { + "ko": "L", + "en": "L" + } + ], + "values_localized": [ + "S", + "M", + "L" + ] + } + ], + "options": [ + { + "id": 7001, + "option_code": "OPT-BLK-S", + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "블랙" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "S" + } + } + ], + "option_values_localized": [ + "블랙", + "S" + ], + "option_name": { + "ko": "블랙 / S" + }, + "option_name_localized": "블랙 / S", + "price_adjustment": 0, + "price_adjustment_formatted": "+0원", + "price_adjustment_type": "increase", + "list_price": 29000, + "list_price_formatted": "29,000원", + "selling_price": 23200, + "selling_price_formatted": "23,200원", + "final_price": 23200, + "final_price_formatted": "23,200원", + "multi_currency_list_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 22.31, + "formatted": "$22.31", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 23200, + "formatted": "23,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.85, + "formatted": "$17.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 0, + "safe_stock_quantity": 10, + "is_below_safe_stock": true, + "is_default": true, + "is_active": true, + "sku": "TS-1001-BLK-S", + "sort_order": 0 + }, + { + "id": 7002, + "option_code": "OPT-BLK-M", + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "블랙" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "M" + } + } + ], + "option_values_localized": [ + "블랙", + "M" + ], + "option_name": { + "ko": "블랙 / M" + }, + "option_name_localized": "블랙 / M", + "price_adjustment": 0, + "price_adjustment_formatted": "+0원", + "price_adjustment_type": "increase", + "list_price": 29000, + "list_price_formatted": "29,000원", + "selling_price": 23200, + "selling_price_formatted": "23,200원", + "final_price": 23200, + "final_price_formatted": "23,200원", + "multi_currency_list_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 22.31, + "formatted": "$22.31", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 23200, + "formatted": "23,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.85, + "formatted": "$17.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 0, + "safe_stock_quantity": 10, + "is_below_safe_stock": true, + "is_default": false, + "is_active": true, + "sku": "TS-1001-BLK-M", + "sort_order": 1 + }, + { + "id": 7003, + "option_code": "OPT-WHT-L", + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "화이트" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "L" + } + } + ], + "option_values_localized": [ + "화이트", + "L" + ], + "option_name": { + "ko": "화이트 / L" + }, + "option_name_localized": "화이트 / L", + "price_adjustment": 2000, + "price_adjustment_formatted": "+2,000원", + "price_adjustment_type": "increase", + "list_price": 31000, + "list_price_formatted": "31,000원", + "selling_price": 25200, + "selling_price_formatted": "25,200원", + "final_price": 25200, + "final_price_formatted": "25,200원", + "multi_currency_list_price": { + "KRW": { + "price": 31000, + "formatted": "31,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 23.85, + "formatted": "$23.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 25200, + "formatted": "25,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 19.38, + "formatted": "$19.38", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 0, + "safe_stock_quantity": 10, + "is_below_safe_stock": true, + "is_default": false, + "is_active": true, + "sku": "TS-1001-WHT-L", + "sort_order": 2 + } + ], + "notice": { + "template_name": "의류 상품정보제공고시", + "values": [ + { + "label": "제품 소재", + "value": "면 100%" + }, + { + "label": "색상", + "value": "블랙, 화이트, 네이비" + }, + { + "label": "치수", + "value": "S, M, L (상세 사이즈표 참고)" + }, + { + "label": "제조자", + "value": "코지홈" + }, + { + "label": "제조국", + "value": "대한민국" + }, + { + "label": "취급 시 주의사항", + "value": "찬물 단독 손세탁 권장" + }, + { + "label": "품질보증기준", + "value": "관련 법 및 소비자분쟁해결기준에 따름" + }, + { + "label": "A/S 책임자와 전화번호", + "value": "코지홈 고객센터 1588-0000" + } + ] + }, + "common_info": { + "name": "배송/교환/환불 안내", + "content": "

주문 후 영업일 기준 1~2일 내 출고됩니다. 단순 변심 교환·반품은 수령 후 7일 이내 가능하며, 왕복 배송비가 부과됩니다.

", + "content_mode": "html" + }, + "is_wishlisted": false + } + } + } + } + }, + { + "id": "suspended", + "label": "$t:editor.state.suspended", + "sampleDataOverrides": { + "byEndpointPattern": { + "/api/modules/sirsoft-ecommerce/products/*": { + "data": { + "id": 101, + "name": { + "ko": "베이직 오버핏 코튼 티셔츠", + "en": "Basic Overfit Cotton T-Shirt" + }, + "name_localized": "베이직 오버핏 코튼 티셔츠", + "product_code": "TS-1001", + "sku": "TS-1001-BLK", + "categories": [ + { + "id": 1, + "name": { + "ko": "패션의류", + "en": "Fashion" + }, + "name_localized": "패션의류", + "breadcrumb": "패션의류", + "path": "1", + "is_primary": false + }, + { + "id": 11, + "name": { + "ko": "상의", + "en": "Tops" + }, + "name_localized": "상의", + "breadcrumb": "패션의류 > 상의", + "path": "1/11", + "is_primary": true + } + ], + "category_name": "상의", + "list_price": 29000, + "list_price_formatted": "29,000원", + "selling_price": 23200, + "selling_price_formatted": "23,200원", + "discount_rate": 20, + "multi_currency_list_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 22.31, + "formatted": "$22.31", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 23200, + "formatted": "23,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.85, + "formatted": "$17.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 320, + "min_purchase_qty": 1, + "max_purchase_qty": 10, + "sales_status": "suspended", + "sales_status_label": "판매중지", + "brand_name": "코지홈", + "labels": [ + { + "name": "BEST", + "color": "#ef4444" + }, + { + "name": "한정특가", + "color": "#f59e0b" + } + ], + "additional_options": [ + { + "id": 9001, + "name": "이니셜 자수 추가", + "is_required": false + }, + { + "id": 9002, + "name": "선물 포장", + "is_required": false + } + ], + "shipping_policy_id": 1, + "free_shipping": false, + "shipping_fee_formatted": "3,000원 (50,000원 이상 무료)", + "shipping_policy": { + "name": "기본 배송정책", + "charge_policy": "conditional_free", + "charge_policy_label": "조건부 무료", + "base_fee": 3000, + "base_fee_formatted": "3,000원", + "free_threshold": 50000, + "free_threshold_formatted": "50,000원", + "fee_summary": "3,000원 (50,000원 이상 무료배송)" + }, + "short_description": "사계절 입기 좋은 오버핏 기본 티셔츠", + "short_description_localized": "사계절 입기 좋은 오버핏 기본 티셔츠", + "description": { + "ko": "

부드러운 30수 코튼 소재로 제작된 데일리 오버핏 티셔츠입니다.

" + }, + "description_localized": "

제품 특징

부드러운 30수 코튼 100% 소재로 제작되어 사계절 내내 편안하게 착용할 수 있습니다. 오버핏 실루엣으로 체형 커버에 탁월하며, 어떤 하의와도 자연스럽게 매치됩니다.

  • 소재: 코튼 100%
  • 핏: 오버핏
  • 제조국: 대한민국

\"상세

", + "description_mode": "html", + "images": [ + { + "id": 5001, + "hash": "img5001", + "original_filename": "tshirt_main.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "정면", + "alt_text_current": "베이직 오버핏 티셔츠 정면", + "is_thumbnail": true, + "sort_order": 0, + "order": 0, + "width": 1000, + "height": 1000, + "file_size": 184320, + "size": 184320, + "size_formatted": "180 KB", + "mime_type": "image/jpeg", + "is_image": true + }, + { + "id": 5002, + "hash": "img5002", + "original_filename": "tshirt_back.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "후면", + "alt_text_current": "베이직 오버핏 티셔츠 후면", + "is_thumbnail": false, + "sort_order": 1, + "order": 1, + "width": 1000, + "height": 1000, + "file_size": 176128, + "size": 176128, + "size_formatted": "172 KB", + "mime_type": "image/jpeg", + "is_image": true + }, + { + "id": 5003, + "hash": "img5003", + "original_filename": "tshirt_detail.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "디테일", + "alt_text_current": "소매 디테일 컷", + "is_thumbnail": false, + "sort_order": 2, + "order": 2, + "width": 1000, + "height": 1000, + "file_size": 158720, + "size": 158720, + "size_formatted": "155 KB", + "mime_type": "image/jpeg", + "is_image": true + } + ], + "thumbnail_url": "/images/placeholder.png", + "meta_title": "베이직 오버핏 코튼 티셔츠 | 코지홈", + "meta_description": "사계절 입기 좋은 오버핏 기본 티셔츠", + "meta_keywords": [ + "티셔츠", + "오버핏", + "코튼" + ], + "has_options": true, + "option_groups": [ + { + "name": { + "ko": "색상", + "en": "Color" + }, + "name_localized": "색상", + "values": [ + { + "ko": "블랙", + "en": "Black" + }, + { + "ko": "화이트", + "en": "White" + }, + { + "ko": "네이비", + "en": "Navy" + } + ], + "values_localized": [ + "블랙", + "화이트", + "네이비" + ] + }, + { + "name": { + "ko": "사이즈", + "en": "Size" + }, + "name_localized": "사이즈", + "values": [ + { + "ko": "S", + "en": "S" + }, + { + "ko": "M", + "en": "M" + }, + { + "ko": "L", + "en": "L" + } + ], + "values_localized": [ + "S", + "M", + "L" + ] + } + ], + "options": [ + { + "id": 7001, + "option_code": "OPT-BLK-S", + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "블랙" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "S" + } + } + ], + "option_values_localized": [ + "블랙", + "S" + ], + "option_name": { + "ko": "블랙 / S" + }, + "option_name_localized": "블랙 / S", + "price_adjustment": 0, + "price_adjustment_formatted": "+0원", + "price_adjustment_type": "increase", + "list_price": 29000, + "list_price_formatted": "29,000원", + "selling_price": 23200, + "selling_price_formatted": "23,200원", + "final_price": 23200, + "final_price_formatted": "23,200원", + "multi_currency_list_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 22.31, + "formatted": "$22.31", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 23200, + "formatted": "23,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.85, + "formatted": "$17.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 80, + "safe_stock_quantity": 10, + "is_below_safe_stock": false, + "is_default": true, + "is_active": true, + "sku": "TS-1001-BLK-S", + "sort_order": 0 + }, + { + "id": 7002, + "option_code": "OPT-BLK-M", + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "블랙" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "M" + } + } + ], + "option_values_localized": [ + "블랙", + "M" + ], + "option_name": { + "ko": "블랙 / M" + }, + "option_name_localized": "블랙 / M", + "price_adjustment": 0, + "price_adjustment_formatted": "+0원", + "price_adjustment_type": "increase", + "list_price": 29000, + "list_price_formatted": "29,000원", + "selling_price": 23200, + "selling_price_formatted": "23,200원", + "final_price": 23200, + "final_price_formatted": "23,200원", + "multi_currency_list_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 22.31, + "formatted": "$22.31", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 23200, + "formatted": "23,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.85, + "formatted": "$17.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 120, + "safe_stock_quantity": 10, + "is_below_safe_stock": false, + "is_default": false, + "is_active": true, + "sku": "TS-1001-BLK-M", + "sort_order": 1 + }, + { + "id": 7003, + "option_code": "OPT-WHT-L", + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "화이트" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "L" + } + } + ], + "option_values_localized": [ + "화이트", + "L" + ], + "option_name": { + "ko": "화이트 / L" + }, + "option_name_localized": "화이트 / L", + "price_adjustment": 2000, + "price_adjustment_formatted": "+2,000원", + "price_adjustment_type": "increase", + "list_price": 31000, + "list_price_formatted": "31,000원", + "selling_price": 25200, + "selling_price_formatted": "25,200원", + "final_price": 25200, + "final_price_formatted": "25,200원", + "multi_currency_list_price": { + "KRW": { + "price": 31000, + "formatted": "31,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 23.85, + "formatted": "$23.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 25200, + "formatted": "25,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 19.38, + "formatted": "$19.38", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 5, + "safe_stock_quantity": 10, + "is_below_safe_stock": true, + "is_default": false, + "is_active": true, + "sku": "TS-1001-WHT-L", + "sort_order": 2 + } + ], + "notice": { + "template_name": "의류 상품정보제공고시", + "values": [ + { + "label": "제품 소재", + "value": "면 100%" + }, + { + "label": "색상", + "value": "블랙, 화이트, 네이비" + }, + { + "label": "치수", + "value": "S, M, L (상세 사이즈표 참고)" + }, + { + "label": "제조자", + "value": "코지홈" + }, + { + "label": "제조국", + "value": "대한민국" + }, + { + "label": "취급 시 주의사항", + "value": "찬물 단독 손세탁 권장" + }, + { + "label": "품질보증기준", + "value": "관련 법 및 소비자분쟁해결기준에 따름" + }, + { + "label": "A/S 책임자와 전화번호", + "value": "코지홈 고객센터 1588-0000" + } + ] + }, + "common_info": { + "name": "배송/교환/환불 안내", + "content": "

주문 후 영업일 기준 1~2일 내 출고됩니다. 단순 변심 교환·반품은 수령 후 7일 이내 가능하며, 왕복 배송비가 부과됩니다.

", + "content_mode": "html" + }, + "is_wishlisted": false + } + } + } + } + }, + { + "id": "coming_soon", + "label": "$t:editor.state.coming_soon", + "sampleDataOverrides": { + "byEndpointPattern": { + "/api/modules/sirsoft-ecommerce/products/*": { + "data": { + "id": 101, + "name": { + "ko": "베이직 오버핏 코튼 티셔츠", + "en": "Basic Overfit Cotton T-Shirt" + }, + "name_localized": "베이직 오버핏 코튼 티셔츠", + "product_code": "TS-1001", + "sku": "TS-1001-BLK", + "categories": [ + { + "id": 1, + "name": { + "ko": "패션의류", + "en": "Fashion" + }, + "name_localized": "패션의류", + "breadcrumb": "패션의류", + "path": "1", + "is_primary": false + }, + { + "id": 11, + "name": { + "ko": "상의", + "en": "Tops" + }, + "name_localized": "상의", + "breadcrumb": "패션의류 > 상의", + "path": "1/11", + "is_primary": true + } + ], + "category_name": "상의", + "list_price": 29000, + "list_price_formatted": "29,000원", + "selling_price": 23200, + "selling_price_formatted": "23,200원", + "discount_rate": 20, + "multi_currency_list_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 22.31, + "formatted": "$22.31", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 23200, + "formatted": "23,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.85, + "formatted": "$17.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 320, + "min_purchase_qty": 1, + "max_purchase_qty": 10, + "sales_status": "coming_soon", + "sales_status_label": "판매예정", + "brand_name": "코지홈", + "labels": [ + { + "name": "BEST", + "color": "#ef4444" + }, + { + "name": "한정특가", + "color": "#f59e0b" + } + ], + "additional_options": [ + { + "id": 9001, + "name": "이니셜 자수 추가", + "is_required": false + }, + { + "id": 9002, + "name": "선물 포장", + "is_required": false + } + ], + "shipping_policy_id": 1, + "free_shipping": false, + "shipping_fee_formatted": "3,000원 (50,000원 이상 무료)", + "shipping_policy": { + "name": "기본 배송정책", + "charge_policy": "conditional_free", + "charge_policy_label": "조건부 무료", + "base_fee": 3000, + "base_fee_formatted": "3,000원", + "free_threshold": 50000, + "free_threshold_formatted": "50,000원", + "fee_summary": "3,000원 (50,000원 이상 무료배송)" + }, + "short_description": "사계절 입기 좋은 오버핏 기본 티셔츠", + "short_description_localized": "사계절 입기 좋은 오버핏 기본 티셔츠", + "description": { + "ko": "

부드러운 30수 코튼 소재로 제작된 데일리 오버핏 티셔츠입니다.

" + }, + "description_localized": "

제품 특징

부드러운 30수 코튼 100% 소재로 제작되어 사계절 내내 편안하게 착용할 수 있습니다. 오버핏 실루엣으로 체형 커버에 탁월하며, 어떤 하의와도 자연스럽게 매치됩니다.

  • 소재: 코튼 100%
  • 핏: 오버핏
  • 제조국: 대한민국

\"상세

", + "description_mode": "html", + "images": [ + { + "id": 5001, + "hash": "img5001", + "original_filename": "tshirt_main.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "정면", + "alt_text_current": "베이직 오버핏 티셔츠 정면", + "is_thumbnail": true, + "sort_order": 0, + "order": 0, + "width": 1000, + "height": 1000, + "file_size": 184320, + "size": 184320, + "size_formatted": "180 KB", + "mime_type": "image/jpeg", + "is_image": true + }, + { + "id": 5002, + "hash": "img5002", + "original_filename": "tshirt_back.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "후면", + "alt_text_current": "베이직 오버핏 티셔츠 후면", + "is_thumbnail": false, + "sort_order": 1, + "order": 1, + "width": 1000, + "height": 1000, + "file_size": 176128, + "size": 176128, + "size_formatted": "172 KB", + "mime_type": "image/jpeg", + "is_image": true + }, + { + "id": 5003, + "hash": "img5003", + "original_filename": "tshirt_detail.jpg", + "download_url": "/images/placeholder.png", + "alt_text": "디테일", + "alt_text_current": "소매 디테일 컷", + "is_thumbnail": false, + "sort_order": 2, + "order": 2, + "width": 1000, + "height": 1000, + "file_size": 158720, + "size": 158720, + "size_formatted": "155 KB", + "mime_type": "image/jpeg", + "is_image": true + } + ], + "thumbnail_url": "/images/placeholder.png", + "meta_title": "베이직 오버핏 코튼 티셔츠 | 코지홈", + "meta_description": "사계절 입기 좋은 오버핏 기본 티셔츠", + "meta_keywords": [ + "티셔츠", + "오버핏", + "코튼" + ], + "has_options": true, + "option_groups": [ + { + "name": { + "ko": "색상", + "en": "Color" + }, + "name_localized": "색상", + "values": [ + { + "ko": "블랙", + "en": "Black" + }, + { + "ko": "화이트", + "en": "White" + }, + { + "ko": "네이비", + "en": "Navy" + } + ], + "values_localized": [ + "블랙", + "화이트", + "네이비" + ] + }, + { + "name": { + "ko": "사이즈", + "en": "Size" + }, + "name_localized": "사이즈", + "values": [ + { + "ko": "S", + "en": "S" + }, + { + "ko": "M", + "en": "M" + }, + { + "ko": "L", + "en": "L" + } + ], + "values_localized": [ + "S", + "M", + "L" + ] + } + ], + "options": [ + { + "id": 7001, + "option_code": "OPT-BLK-S", + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "블랙" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "S" + } + } + ], + "option_values_localized": [ + "블랙", + "S" + ], + "option_name": { + "ko": "블랙 / S" + }, + "option_name_localized": "블랙 / S", + "price_adjustment": 0, + "price_adjustment_formatted": "+0원", + "price_adjustment_type": "increase", + "list_price": 29000, + "list_price_formatted": "29,000원", + "selling_price": 23200, + "selling_price_formatted": "23,200원", + "final_price": 23200, + "final_price_formatted": "23,200원", + "multi_currency_list_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 22.31, + "formatted": "$22.31", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 23200, + "formatted": "23,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.85, + "formatted": "$17.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 80, + "safe_stock_quantity": 10, + "is_below_safe_stock": false, + "is_default": true, + "is_active": true, + "sku": "TS-1001-BLK-S", + "sort_order": 0 + }, + { + "id": 7002, + "option_code": "OPT-BLK-M", + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "블랙" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "M" + } + } + ], + "option_values_localized": [ + "블랙", + "M" + ], + "option_name": { + "ko": "블랙 / M" + }, + "option_name_localized": "블랙 / M", + "price_adjustment": 0, + "price_adjustment_formatted": "+0원", + "price_adjustment_type": "increase", + "list_price": 29000, + "list_price_formatted": "29,000원", + "selling_price": 23200, + "selling_price_formatted": "23,200원", + "final_price": 23200, + "final_price_formatted": "23,200원", + "multi_currency_list_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 22.31, + "formatted": "$22.31", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 23200, + "formatted": "23,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.85, + "formatted": "$17.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 120, + "safe_stock_quantity": 10, + "is_below_safe_stock": false, + "is_default": false, + "is_active": true, + "sku": "TS-1001-BLK-M", + "sort_order": 1 + }, + { + "id": 7003, + "option_code": "OPT-WHT-L", + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "화이트" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "L" + } + } + ], + "option_values_localized": [ + "화이트", + "L" + ], + "option_name": { + "ko": "화이트 / L" + }, + "option_name_localized": "화이트 / L", + "price_adjustment": 2000, + "price_adjustment_formatted": "+2,000원", + "price_adjustment_type": "increase", + "list_price": 31000, + "list_price_formatted": "31,000원", + "selling_price": 25200, + "selling_price_formatted": "25,200원", + "final_price": 25200, + "final_price_formatted": "25,200원", + "multi_currency_list_price": { + "KRW": { + "price": 31000, + "formatted": "31,000원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 23.85, + "formatted": "$23.85", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 25200, + "formatted": "25,200원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 19.38, + "formatted": "$19.38", + "is_default": false, + "editable": false, + "exchange_rate": 1300 + } + }, + "stock_quantity": 5, + "safe_stock_quantity": 10, + "is_below_safe_stock": true, + "is_default": false, + "is_active": true, + "sku": "TS-1001-WHT-L", + "sort_order": 2 + } + ], + "notice": { + "template_name": "의류 상품정보제공고시", + "values": [ + { + "label": "제품 소재", + "value": "면 100%" + }, + { + "label": "색상", + "value": "블랙, 화이트, 네이비" + }, + { + "label": "치수", + "value": "S, M, L (상세 사이즈표 참고)" + }, + { + "label": "제조자", + "value": "코지홈" + }, + { + "label": "제조국", + "value": "대한민국" + }, + { + "label": "취급 시 주의사항", + "value": "찬물 단독 손세탁 권장" + }, + { + "label": "품질보증기준", + "value": "관련 법 및 소비자분쟁해결기준에 따름" + }, + { + "label": "A/S 책임자와 전화번호", + "value": "코지홈 고객센터 1588-0000" + } + ] + }, + "common_info": { + "name": "배송/교환/환불 안내", + "content": "

주문 후 영업일 기준 1~2일 내 출고됩니다. 단순 변심 교환·반품은 수령 후 7일 이내 가능하며, 왕복 배송비가 부과됩니다.

", + "content_mode": "html" + }, + "is_wishlisted": false + } + } + } + } + }, + { + "id": "reviews_tab", + "label": "$t:editor.state.product_reviews_tab", + "comment": "상품상세 리뷰 탭은 _local.activeTab === 'reviews' 게이트 뒤라 기본('info') 상태 캔버스에 미렌더. activeTab 시드 + reviews 를 유저 탭 shape(reviews.data.reviews.data + rating_stats + option_filters + total_count)로 override. base reviews 샘플은 admin 리뷰관리 shape(reviews.data.data[])라 SSoT 분리(copy_source 패턴).", + "initialState": { + "local": { + "activeTab": "reviews" + } + }, + "sampleDataOverrides": { + "byDataSourceId": { + "reviews": { + "data": { + "total_count": 37, + "rating_stats": { + "1": { + "count": 1, + "percent": 3 + }, + "2": { + "count": 2, + "percent": 5 + }, + "3": { + "count": 4, + "percent": 11 + }, + "4": { + "count": 8, + "percent": 22 + }, + "5": { + "count": 22, + "percent": 59 + }, + "avg": 4.3 + }, + "option_filters": [ + { + "key": "색상", + "values": [ + { + "value": "블랙", + "count": 18 + }, + { + "value": "화이트", + "count": 12 + }, + { + "value": "네이비", + "count": 7 + } + ] + }, + { + "key": "사이즈", + "values": [ + { + "value": "S", + "count": 6 + }, + { + "value": "M", + "count": 19 + }, + { + "value": "L", + "count": 12 + } + ] + } + ], + "reviews": { + "data": [ + { + "id": 9101, + "user": { + "name": "김민지", + "avatar": null + }, + "rating": 5, + "content": "핏이 정말 예쁘고 원단도 도톰해서 가을에 입기 딱 좋아요. 세탁 후에도 변형이 없어서 만족합니다.", + "option_snapshot": { + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "블랙" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "M" + } + } + ] + }, + "image_count": 2, + "images": [ + { + "download_url": "/images/placeholder.png" + }, + { + "download_url": "/images/placeholder.png" + } + ], + "has_reply": true, + "reply_content": "소중한 후기 감사합니다! 앞으로도 좋은 상품으로 보답하겠습니다.", + "replied_at": "2026-05-27 09:10", + "created_at": "2026-05-26 11:20" + }, + { + "id": 9102, + "user": { + "name": "이서준", + "avatar": null + }, + "rating": 4, + "content": "색감 예쁘고 무난하게 입기 좋습니다. 다만 생각보다 살짝 얇은 느낌이에요.", + "option_snapshot": { + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "화이트" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "L" + } + } + ] + }, + "image_count": 0, + "images": [], + "has_reply": false, + "reply_content": null, + "replied_at": null, + "created_at": "2026-05-24 16:42" + }, + { + "id": 9103, + "user": { + "name": "박도윤", + "avatar": null + }, + "rating": 5, + "content": "재구매했어요. 데일리로 입기 좋고 색상별로 사고 싶네요. 배송도 빨랐습니다.", + "option_snapshot": { + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "네이비" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "S" + } + } + ] + }, + "image_count": 5, + "images": [ + { + "download_url": "/images/placeholder.png" + }, + { + "download_url": "/images/placeholder.png" + }, + { + "download_url": "/images/placeholder.png" + }, + { + "download_url": "/images/placeholder.png" + }, + { + "download_url": "/images/placeholder.png" + } + ], + "has_reply": true, + "reply_content": "재구매 감사합니다! 네이비 색상도 곧 재입고 예정입니다.", + "replied_at": "2026-05-21 10:00", + "created_at": "2026-05-20 19:05" + }, + { + "id": 9104, + "user": { + "name": "최예린", + "avatar": null + }, + "rating": 3, + "content": "무난해요. 기본템으로는 괜찮은데 특별하진 않네요.", + "option_snapshot": { + "option_values": [ + { + "key": { + "ko": "색상" + }, + "value": { + "ko": "블랙" + } + }, + { + "key": { + "ko": "사이즈" + }, + "value": { + "ko": "M" + } + } + ] + }, + "image_count": 0, + "images": [], + "has_reply": false, + "reply_content": null, + "replied_at": null, + "created_at": "2026-05-18 08:33" + } + ], + "meta": { + "current_page": 1, + "last_page": 4, + "per_page": 10, + "total": 37 + } + } + } + } + } + } + }, + { + "id": "qna_tab", + "label": "$t:editor.state.product_qna_tab", + "comment": "상품상세 문의 탭은 _local.activeTab === 'qna' && _global.modules['sirsoft-ecommerce'].inquiry.board_slug 게이트 + 탭 hiddenTabIds 가 board_slug 없으면 qna 탭 숨김. activeTab + global modules.inquiry.board_slug 시드 + qna 를 유저 shape(items[] + meta)로 override(기존 stub items:['샘플'] 해소).", + "initialState": { + "local": { + "activeTab": "qna" + }, + "global": { + "modules": { + "sirsoft-ecommerce": { + "inquiry": { + "board_slug": "product-qna" + } + } + } + } + } + } + ] + }, + { + "comment": "shop/index — 목록 유무 + 접근 제한. _local.productAccessDenied=true 시 접근 제한 화면을 if 분기한다(본문 line 192).", + "scope": { + "kind": "route", + "match": "/shop/products" + }, + "items": [ + { + "id": "with_data", + "label": "$t:editor.state.with_data", + "default": true + }, + { + "id": "empty_data", + "label": "$t:editor.state.empty_data", + "sampleDataOverrides": { + "byEndpointPattern": { + "/api/modules/sirsoft-ecommerce/products*": { + "data": { + "data": [], + "meta": { + "total": 0, + "per_page": 20, + "current_page": 1 + } + } + } + } + } + }, + { + "id": "access_denied", + "label": "$t:editor.state.shop_access_denied", + "initialState": { + "local": { + "productAccessDenied": true + } + } + } + ] + }, + { + "comment": "shop/cart — 담긴 상품 유무 + 주문 처리 중(_local.isOrdering 시 오버레이 잠금, 본문 line 115).", + "scope": { + "kind": "route", + "match": "/shop/cart" + }, + "items": [ + { + "id": "with_items", + "label": "$t:editor.state.cart_with_items", + "default": true + }, + { + "id": "empty_cart", + "label": "$t:editor.state.cart_empty", + "sampleDataOverrides": { + "byDataSourceId": { + "cartItems": { + "data": { + "items": [], + "item_ids": [], + "item_count": 0, + "calculation": { + "items": [], + "promotions": { + "product_promotions": { + "coupons": [], + "discount_codes": [], + "events": [] + }, + "order_promotions": { + "coupons": [], + "discount_codes": [], + "events": [] + } + }, + "validation_errors": [], + "summary": { + "subtotal": 0, + "subtotal_formatted": "0원", + "total_discount": 0, + "discount_formatted": "0원", + "total_shipping": 0, + "shipping_fee_formatted": "0원", + "final_amount": 0, + "final_amount_formatted": "0원", + "payment_amount_formatted": "0원", + "points_earning": 0, + "multi_currency": [] + } + } + } + } + } + } + }, + { + "id": "ordering", + "label": "$t:editor.state.cart_ordering", + "initialState": { + "local": { + "isOrdering": true + } + } + }, + { + "id": "summary_tax_expanded", + "label": "$t:editor.state.cart_summary_details", + "comment": "장바구니 요약의 세금 상세는 (1) 금액 영역이 _local.selectedItems.length>0 게이트(_cart_summary.json line 75), (2) 세금 상세가 _local.showTaxDetails 토글 게이트(line 128)로 이중 중첩. 둘 다 시드(selectedItems=[1,2,3] + showTaxDetails:true)해 과세/면세 금액(multi_currency) breakdown 을 노출. 편집기는 정적 시뮬레이션이라 체크박스 클릭·토글 클릭 불가.", + "initialState": { + "local": { + "showTaxDetails": true, + "selectedItems": [ + 1, + 2, + 3 + ] + } + } + } + ] + }, + { + "comment": "shop/checkout — 정상/결제오류(_local.orderError·query.error, 본문 line 45/194)/구매불가(_local.unavailableModal, line 102)/입력검증실패 분기. 검증 실패는 주문자/배송지 입력칸별 인라인 에러를 _local.errors 의 점 포함 flat 키(예 'orderer.name')로 읽는다(_checkout_orderer.json line 99/101, _checkout_shipping.json) — formErrors 키를 대괄호 표기로 지정한다.", + "scope": { + "kind": "route", + "match": "/shop/checkout" + }, + "items": [ + { + "id": "normal", + "label": "$t:editor.state.checkout_normal", + "default": true, + "comment": "빈 local 패치 — sampleGlobal._local baseline(받는분/주소/배송메모 시드)을 캔버스에 force-inject 하는 트리거. 패치가 없으면 편집기가 _localInit 을 주입하지 않아 레이아웃 자체 initLocal 이 baseline 을 덮어쓴다. 시드 데이터는 sampleGlobal._local 1곳에만 존재(상태별 중복 없음).", + "initialState": { + "local": [] + } + }, + { + "id": "payment_error", + "label": "$t:editor.state.checkout_payment_error", + "initialState": { + "local": { + "orderError": true, + "orderErrorMessage": "결제 처리 중 오류가 발생했습니다" + }, + "query": { + "error": "confirm_failed" + } + } + }, + { + "id": "validation_failed", + "label": "$t:editor.state.checkout_validation_failed", + "formErrors": { + "_local.errors['orderer.name']": [ + "주문자 이름을 입력해 주세요" + ], + "_local.errors['orderer.phone']": [ + "연락처를 입력해 주세요" + ], + "_local.errors['orderer.email']": [ + "올바른 이메일 형식이 아닙니다" + ], + "_local.errors['shipping.recipient_name']": [ + "받는 분 이름을 입력해 주세요" + ], + "_local.errors['shipping.recipient_phone']": [ + "받는 분 연락처를 입력해 주세요" + ], + "_local.errors['shipping.zipcode']": [ + "우편번호를 입력해 주세요" + ], + "_local.errors['shipping.address']": [ + "주소를 입력해 주세요" + ] + } + }, + { + "id": "summary_details_expanded", + "label": "$t:editor.state.checkout_summary_details", + "comment": "결제금액 요약의 세금/할인 상세는 _local.showTaxDetails / showDiscountDetails 토글 게이트 뒤(_checkout_summary.json line 93/161)라 기본 상태 미렌더. 두 토글 ON 으로 과세/면세 금액·상품/주문/배송 쿠폰 할인 breakdown(multi_currency 전 경로) 노출. base checkoutData 가 풍부하므로 override 불요.", + "initialState": { + "local": { + "showTaxDetails": true, + "showDiscountDetails": true + } + } + } + ] + }, + { + "comment": "shop/order_complete — orderData.data.payment.payment_method 값(card/vbank/dbank)으로 결제수단별 안내를 if 분기한다(본문 line 52/95/138/229). card 는 base sampleData(=card)가 흐르므로 오버라이드 없음, vbank/dbank 는 byDataSourceId.orderData 통째 교체.", + "scope": { + "kind": "route", + "match": "/shop/orders/:id/complete" + }, + "items": [ + { + "id": "card", + "label": "$t:editor.state.payment_card", + "default": true + }, + { + "id": "vbank", + "label": "$t:editor.state.payment_vbank", + "sampleDataOverrides": { + "byDataSourceId": { + "orderData": { + "data": { + "id": 5001, + "order_number": "20260603-0001", + "order_status": "payment_pending", + "order_status_label": "입금대기", + "order_status_variant": "warning", + "options": [ + { + "id": 9001, + "option_status": "preparing", + "option_status_label": "배송준비중", + "option_status_variant": "info", + "product_id": 1, + "product_option_id": 101, + "sku": "EARBUDS-PRO-BLK", + "product_name": "프리미엄 무선 이어버드 프로", + "product_option_name": "미드나잇 블랙 / 기본", + "option_name": "미드나잇 블랙 / 기본", + "quantity": 2, + "shipped_quantity": 0, + "unit_price": 29000, + "unit_price_formatted": "29,000원", + "subtotal_price": 58000, + "subtotal_price_formatted": "58,000원", + "subtotal_discount_amount": 5000, + "subtotal_discount_amount_formatted": "5,000원", + "list_price": 32000, + "list_price_formatted": "32,000원", + "final_amount": 53000, + "final_amount_formatted": "53,000원", + "thumbnail_url": null, + "mc_unit_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true + }, + "USD": { + "price": 21.3, + "formatted": "$21.30", + "is_default": false + } + }, + "mc_subtotal_price": { + "KRW": { + "price": 58000, + "formatted": "58,000원", + "is_default": true + }, + "USD": { + "price": 42.6, + "formatted": "$42.60", + "is_default": false + } + }, + "confirmed_at": null, + "can_confirm": false, + "can_write_review": false, + "has_review": false + }, + { + "id": 9002, + "option_status": "preparing", + "option_status_label": "배송준비중", + "option_status_variant": "info", + "product_id": 2, + "product_option_id": 102, + "sku": "CABLE-15M-WHT", + "product_name": "고속 충전 케이블 1.5m", + "product_option_name": "1.5m", + "option_name": "1.5m", + "quantity": 1, + "shipped_quantity": 0, + "unit_price": 18000, + "unit_price_formatted": "18,000원", + "subtotal_price": 18000, + "subtotal_price_formatted": "18,000원", + "subtotal_discount_amount": 0, + "subtotal_discount_amount_formatted": "0원", + "list_price": 18000, + "list_price_formatted": "18,000원", + "final_amount": 18000, + "final_amount_formatted": "18,000원", + "thumbnail_url": null, + "mc_unit_price": { + "KRW": { + "price": 18000, + "formatted": "18,000원", + "is_default": true + }, + "USD": { + "price": 13.2, + "formatted": "$13.20", + "is_default": false + } + }, + "mc_subtotal_price": { + "KRW": { + "price": 18000, + "formatted": "18,000원", + "is_default": true + }, + "USD": { + "price": 13.2, + "formatted": "$13.20", + "is_default": false + } + }, + "confirmed_at": null, + "can_confirm": false, + "can_write_review": false, + "has_review": false + } + ], + "payment": { + "id": 8001, + "payment_status": "pending", + "payment_status_label": "입금대기", + "payment_status_variant": "warning", + "payment_method": "vbank", + "payment_method_label": "가상계좌", + "payment_type_label": "일반결제", + "pg_provider": "kginicis", + "payment_number": "PAY-20260603-0002", + "paid_amount_formatted": "102,000원", + "card_name": null, + "card_number_masked": null, + "card_installment_months": 0, + "is_interest_free": false, + "paid_at_formatted": null, + "dbank_name": null, + "dbank_account": null, + "dbank_holder": null, + "depositor_name": null, + "deposit_due_at_formatted": null, + "vbank_name": "국민은행", + "vbank_number": "901234-56-789012", + "vbank_holder": "(주)지세븐커머스", + "vbank_due_at": "2026-06-05T23:59:59+09:00" + }, + "shipping_address": { + "id": 7001, + "address_type": "shipping", + "recipient_name": "홍길동", + "recipient_phone": "010-1234-5678", + "recipient_country_code": "KR", + "zipcode": "06234", + "address": "서울특별시 강남구 테헤란로 123", + "address_detail": "4층 401호", + "city": "", + "state": "", + "full_address": "서울특별시 강남구 테헤란로 123 4층 401호", + "delivery_memo": "부재 시 경비실에 맡겨주세요" + }, + "subtotal_amount": 76000, + "subtotal_amount_formatted": "76,000원", + "total_discount_amount": 5000, + "total_discount_amount_formatted": "5,000원", + "total_shipping_amount": 0, + "total_shipping_amount_formatted": "0원", + "total_amount": 102000, + "total_amount_formatted": "102,000원", + "mc_subtotal_amount": { + "KRW": { + "price": 76000, + "formatted": "76,000원", + "is_default": true + }, + "USD": { + "price": 55.7, + "formatted": "$55.70", + "is_default": false + } + }, + "mc_total_discount_amount": { + "KRW": { + "price": 5000, + "formatted": "5,000원", + "is_default": true + }, + "USD": { + "price": 3.65, + "formatted": "$3.65", + "is_default": false + } + }, + "mc_total_shipping_amount": { + "KRW": { + "price": 0, + "formatted": "0원", + "is_default": true + }, + "USD": { + "price": 0, + "formatted": "$0.00", + "is_default": false + } + }, + "mc_total_amount": { + "KRW": { + "price": 102000, + "formatted": "102,000원", + "is_default": true + }, + "USD": { + "price": 74.5, + "formatted": "$74.50", + "is_default": false + } + } + } + } + } + } + }, + { + "id": "dbank", + "label": "$t:editor.state.payment_dbank", + "sampleDataOverrides": { + "byDataSourceId": { + "orderData": { + "data": { + "id": 5001, + "order_number": "20260603-0001", + "order_status": "payment_pending", + "order_status_label": "입금대기", + "order_status_variant": "warning", + "options": [ + { + "id": 9001, + "option_status": "preparing", + "option_status_label": "배송준비중", + "option_status_variant": "info", + "product_id": 1, + "product_option_id": 101, + "sku": "EARBUDS-PRO-BLK", + "product_name": "프리미엄 무선 이어버드 프로", + "product_option_name": "미드나잇 블랙 / 기본", + "option_name": "미드나잇 블랙 / 기본", + "quantity": 2, + "shipped_quantity": 0, + "unit_price": 29000, + "unit_price_formatted": "29,000원", + "subtotal_price": 58000, + "subtotal_price_formatted": "58,000원", + "subtotal_discount_amount": 5000, + "subtotal_discount_amount_formatted": "5,000원", + "list_price": 32000, + "list_price_formatted": "32,000원", + "final_amount": 53000, + "final_amount_formatted": "53,000원", + "thumbnail_url": null, + "mc_unit_price": { + "KRW": { + "price": 29000, + "formatted": "29,000원", + "is_default": true + }, + "USD": { + "price": 21.3, + "formatted": "$21.30", + "is_default": false + } + }, + "mc_subtotal_price": { + "KRW": { + "price": 58000, + "formatted": "58,000원", + "is_default": true + }, + "USD": { + "price": 42.6, + "formatted": "$42.60", + "is_default": false + } + }, + "confirmed_at": null, + "can_confirm": false, + "can_write_review": false, + "has_review": false + }, + { + "id": 9002, + "option_status": "preparing", + "option_status_label": "배송준비중", + "option_status_variant": "info", + "product_id": 2, + "product_option_id": 102, + "sku": "CABLE-15M-WHT", + "product_name": "고속 충전 케이블 1.5m", + "product_option_name": "1.5m", + "option_name": "1.5m", + "quantity": 1, + "shipped_quantity": 0, + "unit_price": 18000, + "unit_price_formatted": "18,000원", + "subtotal_price": 18000, + "subtotal_price_formatted": "18,000원", + "subtotal_discount_amount": 0, + "subtotal_discount_amount_formatted": "0원", + "list_price": 18000, + "list_price_formatted": "18,000원", + "final_amount": 18000, + "final_amount_formatted": "18,000원", + "thumbnail_url": null, + "mc_unit_price": { + "KRW": { + "price": 18000, + "formatted": "18,000원", + "is_default": true + }, + "USD": { + "price": 13.2, + "formatted": "$13.20", + "is_default": false + } + }, + "mc_subtotal_price": { + "KRW": { + "price": 18000, + "formatted": "18,000원", + "is_default": true + }, + "USD": { + "price": 13.2, + "formatted": "$13.20", + "is_default": false + } + }, + "confirmed_at": null, + "can_confirm": false, + "can_write_review": false, + "has_review": false + } + ], + "payment": { + "id": 8001, + "payment_status": "pending", + "payment_status_label": "입금대기", + "payment_status_variant": "warning", + "payment_method": "dbank", + "payment_method_label": "무통장입금", + "payment_type_label": "일반결제", + "pg_provider": "kginicis", + "payment_number": "PAY-20260603-0003", + "paid_amount_formatted": "102,000원", + "card_name": null, + "card_number_masked": null, + "card_installment_months": 0, + "is_interest_free": false, + "paid_at_formatted": null, + "dbank_name": "신한은행", + "dbank_account": "110-123-456789", + "dbank_holder": "(주)지세븐커머스", + "depositor_name": "홍길동", + "deposit_due_at_formatted": "2026-06-05 23:59", + "vbank_name": null, + "vbank_number": null, + "vbank_holder": null, + "vbank_due_at": null + }, + "shipping_address": { + "id": 7001, + "address_type": "shipping", + "recipient_name": "홍길동", + "recipient_phone": "010-1234-5678", + "recipient_country_code": "KR", + "zipcode": "06234", + "address": "서울특별시 강남구 테헤란로 123", + "address_detail": "4층 401호", + "city": "", + "state": "", + "full_address": "서울특별시 강남구 테헤란로 123 4층 401호", + "delivery_memo": "부재 시 경비실에 맡겨주세요" + }, + "subtotal_amount": 76000, + "subtotal_amount_formatted": "76,000원", + "total_discount_amount": 5000, + "total_discount_amount_formatted": "5,000원", + "total_shipping_amount": 0, + "total_shipping_amount_formatted": "0원", + "total_amount": 102000, + "total_amount_formatted": "102,000원", + "mc_subtotal_amount": { + "KRW": { + "price": 76000, + "formatted": "76,000원", + "is_default": true + }, + "USD": { + "price": 55.7, + "formatted": "$55.70", + "is_default": false + } + }, + "mc_total_discount_amount": { + "KRW": { + "price": 5000, + "formatted": "5,000원", + "is_default": true + }, + "USD": { + "price": 3.65, + "formatted": "$3.65", + "is_default": false + } + }, + "mc_total_shipping_amount": { + "KRW": { + "price": 0, + "formatted": "0원", + "is_default": true + }, + "USD": { + "price": 0, + "formatted": "$0.00", + "is_default": false + } + }, + "mc_total_amount": { + "KRW": { + "price": 102000, + "formatted": "102,000원", + "is_default": true + }, + "USD": { + "price": 74.5, + "formatted": "$74.50", + "is_default": false + } + } + } + } + } + } + } + ] + }, + { + "comment": "shop/reorder — _local.status 값(pending/success/error)으로 재주문 진행 화면을 if 분기한다(본문 line 72/99/225).", + "scope": { + "kind": "route", + "match": "/shop/reorder/:id" + }, + "items": [ + { + "id": "pending", + "label": "$t:editor.state.reorder_pending", + "default": true, + "initialState": { + "local": { + "status": "pending" + } + } + }, + { + "id": "success_partial", + "label": "$t:editor.state.reorder_success_partial", + "initialState": { + "local": { + "status": "success", + "skipped": [ + { + "name": "품절 상품" + } + ] + } + } + }, + { + "id": "error", + "label": "$t:editor.state.reorder_error", + "initialState": { + "local": { + "status": "error" + } + } + } + ] + }, + { + "comment": "admin 상품 폼 — route.itemCode 유무로 수정/신규, query.copy_id 로 복사 생성을 분기한다(본문 line 9/32/75). 검증 오류는 _local.errors.{field} 경로.", + "scope": { + "kind": "route", + "match": "*/admin/ecommerce/products/:itemCode/edit" + }, + "items": [ + { + "id": "edit_mode", + "label": "$t:editor.state.edit_existing", + "default": true + }, + { + "id": "create_mode", + "label": "$t:editor.state.create_mode", + "initialState": { + "route": { + "itemCode": null + } + } + }, + { + "id": "validation_failed", + "label": "$t:editor.state.validation_failed", + "formErrors": { + "_local.errors.name": [ + "필수 입력 항목입니다" + ] + } + } + ] + }, + { + "comment": "admin 쿠폰 폼 — route.id 유무로 수정/신규 분기(본문 line 9/74). 검증 오류는 _local.errors.", + "scope": { + "kind": "route", + "match": "*/admin/ecommerce/promotion-coupons/:id/edit" + }, + "items": [ + { + "id": "edit_mode", + "label": "$t:editor.state.edit_existing", + "default": true, + "initialState": { + "local": { + "form": { + "target_type": "order_amount", + "name": { + "ko": "신규회원 10% 할인", + "en": "New Member 10% Off" + }, + "description": { + "ko": "신규 가입 회원 대상 주문금액 10% 할인 쿠폰", + "en": "10% off for new members" + }, + "issue_method": "download", + "issue_condition": "manual", + "discount_type": "rate", + "discount_value": 10, + "discount_max_amount": 20000, + "min_order_amount": 30000, + "valid_type": "days", + "valid_days": 30, + "valid_from": null, + "valid_to": null, + "total_quantity": 1000, + "issue_from": "2026-05-01T00:00", + "issue_to": "2026-06-30T23:59", + "per_user_limit": 1, + "is_combinable": false, + "target_scope": "all" + } + } + } + }, + { + "id": "create_mode", + "label": "$t:editor.state.create_mode", + "initialState": { + "route": { + "id": null + } + } + }, + { + "id": "validation_failed", + "label": "$t:editor.state.validation_failed", + "formErrors": { + "_local.errors.name": [ + "필수 입력 항목입니다" + ] + } + } + ] + }, + { + "comment": "admin 배송 정책 폼 — route.id 유무로 수정/신규, query.copy_id 로 복사 분기(본문 line 9/60/98). 검증 오류는 _local.errors.", + "scope": { + "kind": "route", + "match": "*/admin/ecommerce/shipping-policies/:id/edit" + }, + "items": [ + { + "id": "edit_mode", + "label": "부과정책: 조건부 무료", + "default": true, + "initialState": { + "local": { + "form": { + "name": { + "ko": "기본 배송정책", + "en": "Standard Shipping" + }, + "is_active": true, + "is_default": true, + "country_settings": [ + { + "country_code": "KR", + "shipping_method": "parcel", + "custom_shipping_name": null, + "carrier": null, + "currency_code": "KRW", + "charge_policy": "conditional_free", + "base_fee": 3000, + "free_threshold": 50000, + "ranges": null, + "api_endpoint": null, + "api_request_fields": null, + "api_response_fee_field": null, + "extra_fee_enabled": true, + "extra_fee_settings": [ + { + "region": "제주특별자치도", + "zipcode": "63000-63644", + "fee": 3000 + }, + { + "region": "도서산간", + "zipcode": "", + "fee": 4000 + } + ], + "extra_fee_multiply": false, + "is_active": true + }, + { + "country_code": "US", + "shipping_method": "parcel", + "custom_shipping_name": null, + "carrier": null, + "currency_code": "USD", + "charge_policy": "range_amount", + "base_fee": 0, + "free_threshold": null, + "ranges": { + "type": "amount", + "tiers": [ + { + "min": 0, + "max": 50000, + "fee": 3000 + }, + { + "min": 50000, + "max": 100000, + "fee": 1500 + }, + { + "min": 100000, + "max": null, + "fee": 0 + } + ] + }, + "api_endpoint": null, + "api_request_fields": null, + "api_response_fee_field": null, + "extra_fee_enabled": false, + "extra_fee_settings": [], + "extra_fee_multiply": false, + "is_active": true + } + ] + }, + "activeCountryTab": 0, + "rangeErrors": [], + "showBaseFee": true, + "showFreeThreshold": true, + "showRanges": false, + "showApiSettings": false, + "showUnitValue": false + } + } + }, + { + "id": "charge_fixed", + "label": "부과정책: 고정", + "default": false, + "initialState": { + "local": { + "form": { + "name": { + "ko": "기본 배송정책", + "en": "Standard Shipping" + }, + "is_active": true, + "is_default": false, + "country_settings": [ + { + "country_code": "KR", + "shipping_method": "parcel", + "custom_shipping_name": null, + "carrier": null, + "currency_code": "KRW", + "charge_policy": "fixed", + "base_fee": 3000, + "free_threshold": null, + "ranges": null, + "api_endpoint": null, + "api_request_fields": null, + "api_response_fee_field": null, + "extra_fee_enabled": false, + "extra_fee_settings": [], + "extra_fee_multiply": false, + "is_active": true + } + ] + }, + "activeCountryTab": 0, + "rangeErrors": [], + "showBaseFee": true, + "showFreeThreshold": false, + "showRanges": false, + "showApiSettings": false, + "showUnitValue": false + } + } + }, + { + "id": "charge_range", + "label": "부과정책: 구간별(금액)", + "default": false, + "initialState": { + "local": { + "form": { + "name": { + "ko": "기본 배송정책", + "en": "Standard Shipping" + }, + "is_active": true, + "is_default": false, + "country_settings": [ + { + "country_code": "KR", + "shipping_method": "parcel", + "custom_shipping_name": null, + "carrier": null, + "currency_code": "KRW", + "charge_policy": "range_amount", + "base_fee": 0, + "free_threshold": null, + "ranges": { + "type": "amount", + "tiers": [ + { + "min": 0, + "max": 50000, + "fee": 3000 + }, + { + "min": 50000, + "max": 100000, + "fee": 1500 + }, + { + "min": 100000, + "max": null, + "fee": 0 + } + ] + }, + "api_endpoint": null, + "api_request_fields": null, + "api_response_fee_field": null, + "extra_fee_enabled": false, + "extra_fee_settings": [], + "extra_fee_multiply": false, + "is_active": true + } + ] + }, + "activeCountryTab": 0, + "rangeErrors": [], + "showBaseFee": false, + "showFreeThreshold": false, + "showRanges": true, + "showApiSettings": false, + "showUnitValue": false + } + } + }, + { + "id": "charge_per_weight", + "label": "부과정책: 무게당", + "default": false, + "initialState": { + "local": { + "form": { + "name": { + "ko": "기본 배송정책", + "en": "Standard Shipping" + }, + "is_active": true, + "is_default": false, + "country_settings": [ + { + "country_code": "KR", + "shipping_method": "parcel", + "custom_shipping_name": null, + "carrier": null, + "currency_code": "KRW", + "charge_policy": "per_weight", + "base_fee": 2500, + "free_threshold": null, + "ranges": { + "type": "weight", + "unit_value": 5 + }, + "api_endpoint": null, + "api_request_fields": null, + "api_response_fee_field": null, + "extra_fee_enabled": false, + "extra_fee_settings": [], + "extra_fee_multiply": false, + "is_active": true + } + ] + }, + "activeCountryTab": 0, + "rangeErrors": [], + "showBaseFee": true, + "showFreeThreshold": false, + "showRanges": false, + "showApiSettings": false, + "showUnitValue": true + } + } + }, + { + "id": "charge_free", + "label": "부과정책: 무료", + "default": false, + "initialState": { + "local": { + "form": { + "name": { + "ko": "기본 배송정책", + "en": "Standard Shipping" + }, + "is_active": true, + "is_default": false, + "country_settings": [ + { + "country_code": "KR", + "shipping_method": "parcel", + "custom_shipping_name": null, + "carrier": null, + "currency_code": "KRW", + "charge_policy": "free", + "base_fee": 0, + "free_threshold": null, + "ranges": null, + "api_endpoint": null, + "api_request_fields": null, + "api_response_fee_field": null, + "extra_fee_enabled": false, + "extra_fee_settings": [], + "extra_fee_multiply": false, + "is_active": true + } + ] + }, + "activeCountryTab": 0, + "rangeErrors": [], + "showBaseFee": false, + "showFreeThreshold": false, + "showRanges": false, + "showApiSettings": false, + "showUnitValue": false + } + } + }, + { + "id": "charge_api", + "label": "부과정책: 계산 API", + "default": false, + "initialState": { + "local": { + "form": { + "name": { + "ko": "기본 배송정책", + "en": "Standard Shipping" + }, + "is_active": true, + "is_default": false, + "country_settings": [ + { + "country_code": "KR", + "shipping_method": "parcel", + "custom_shipping_name": null, + "carrier": null, + "currency_code": "KRW", + "charge_policy": "api", + "base_fee": 0, + "free_threshold": null, + "ranges": null, + "api_endpoint": "https://shipping.example.com/api/quote", + "api_request_fields": [ + "zipcode", + "weight" + ], + "api_response_fee_field": "fee", + "extra_fee_enabled": false, + "extra_fee_settings": [], + "extra_fee_multiply": false, + "is_active": true + } + ] + }, + "activeCountryTab": 0, + "rangeErrors": [], + "showBaseFee": false, + "showFreeThreshold": false, + "showRanges": false, + "showApiSettings": true, + "showUnitValue": false + } + } + }, + { + "id": "create_mode", + "label": "$t:editor.state.create_mode", + "initialState": { + "route": { + "id": null + } + } + }, + { + "id": "validation_failed", + "label": "$t:editor.state.validation_failed", + "formErrors": { + "_local.errors.name": [ + "필수 입력 항목입니다" + ] + } + } + ] + }, + { + "comment": "admin 이커머스 설정 — {{_global.activeEcommerceSettingsTab || query.tab || 'basic_info'}} 으로 9개 탭 화면을 if 분기한다(본문 line 303/344~). query.tab + 우선 키 함께 주입.", + "scope": { + "kind": "route", + "match": "*/admin/ecommerce/settings" + }, + "items": [ + { + "id": "tab_basic_info", + "label": "$t:editor.state.tab.basic_info", + "initialState": { + "query": { + "tab": "basic_info" + }, + "global": { + "activeEcommerceSettingsTab": "basic_info" + } + }, + "default": true + }, + { + "id": "tab_language_currency", + "label": "$t:editor.state.tab.language_currency", + "initialState": { + "query": { + "tab": "language_currency" + }, + "global": { + "activeEcommerceSettingsTab": "language_currency" + } + } + }, + { + "id": "tab_seo", + "label": "$t:editor.state.tab.seo", + "initialState": { + "query": { + "tab": "seo" + }, + "global": { + "activeEcommerceSettingsTab": "seo" + } + } + }, + { + "id": "tab_order_settings", + "label": "$t:editor.state.tab.order_settings", + "initialState": { + "query": { + "tab": "order_settings" + }, + "global": { + "activeEcommerceSettingsTab": "order_settings" + } + } + }, + { + "id": "tab_claim", + "label": "$t:editor.state.tab.claim", + "initialState": { + "query": { + "tab": "claim" + }, + "global": { + "activeEcommerceSettingsTab": "claim" + } + } + }, + { + "id": "tab_shipping", + "label": "$t:editor.state.tab.shipping", + "initialState": { + "query": { + "tab": "shipping" + }, + "global": { + "activeEcommerceSettingsTab": "shipping" + } + } + }, + { + "id": "tab_review_settings", + "label": "$t:editor.state.tab.review_settings", + "initialState": { + "query": { + "tab": "review_settings" + }, + "global": { + "activeEcommerceSettingsTab": "review_settings" + } + } + }, + { + "id": "tab_notification_definitions", + "label": "$t:editor.state.tab.notification_definitions", + "initialState": { + "query": { + "tab": "notification_definitions" + }, + "global": { + "activeEcommerceSettingsTab": "notification_definitions" + } + } + }, + { + "id": "tab_identity_policies", + "label": "$t:editor.state.tab.identity_policies", + "initialState": { + "query": { + "tab": "identity_policies" + }, + "global": { + "activeEcommerceSettingsTab": "identity_policies" + } + } + } + ] + }, + { + "comment": "admin 마일리지/예치금 설정 — _local.activeTab(mileage/deposit)으로 탭을 분기한다(본문 line 130/165).", + "scope": { + "kind": "route", + "match": "*/admin/ecommerce/mileage-deposit-settings" + }, + "items": [ + { + "id": "tab_mileage", + "label": "$t:editor.state.tab.mileage", + "default": true, + "initialState": { + "local": { + "activeTab": "mileage" + } + } + }, + { + "id": "tab_deposit", + "label": "$t:editor.state.tab.deposit", + "initialState": { + "local": { + "activeTab": "deposit" + } + } + } + ] + }, + { + "comment": "shop/guest_order_form — 비회원 주문 조회 폼. 본문이 _global.currentUser.uuid 유무로 양분된다(slots.content[0] 회원→마이페이지 리다이렉트 안내 / content[1] 비회원→조회 폼). 편집기 sampleGlobal 은 로그인 상태(currentUser 시드)라 비회원 조회 폼이 기본 미표시이므로, currentUser 를 null 로 패치한 비회원 상태를 기본으로 노출해 폼을 편집 가능하게 한다.", + "scope": { + "kind": "route", + "match": "/shop/guest/orders" + }, + "items": [ + { + "id": "guest", + "label": "$t:editor.state.guest_order_form_guest", + "default": true, + "comment": "비회원 상태 — currentUser 를 null 로 덮어 content[1] 조회 폼을 캔버스에 노출한다.", + "initialState": { + "global": { + "currentUser": null + } + } + }, + { + "id": "member", + "label": "$t:editor.state.guest_order_form_member", + "comment": "회원 로그인 상태 — currentUser.uuid 를 명시 시드해 content[0] 마이페이지 리다이렉트 안내 분기를 활성화한다. sampleGlobal 은 코어 keyspace(currentUser)를 시드하지 않으므로(코어 우선) 빈 패치로는 guest(null) 와 다른 결과를 만들지 못한다 — 로그인 상태를 명시 패치해야 양분 분기가 회원 쪽으로 전환되고 캔버스가 실제로 달라진다.", + "initialState": { + "global": { + "currentUser": { + "uuid": "editor-sample-member-uuid" + } + } + } + } + ] + }, + { + "comment": "admin 마일리지 거래내역 목록 — transactions 데이터소스 목록 유무. 마일리지 시스템.", + "scope": { + "kind": "route", + "match": "*/admin/ecommerce/mileage-transactions" + }, + "items": [ + { + "id": "with_data", + "label": "$t:editor.state.with_data", + "default": true + }, + { + "id": "empty_data", + "label": "$t:editor.state.empty_data", + "sampleDataOverrides": { + "byEndpointPattern": { + "/api/modules/sirsoft-ecommerce/admin/mileage-transactions*": { + "data": { + "data": [], + "meta": { + "total": 0, + "per_page": 20, + "current_page": 1 + } + } + } + } + } + } + ] + }, + { + "comment": "admin 쿠폰 목록 — coupons 데이터소스 목록 유무. 쿠폰 관리.", + "scope": { + "kind": "route", + "match": "*/admin/ecommerce/promotion-coupons" + }, + "items": [ + { + "id": "with_data", + "label": "$t:editor.state.with_data", + "default": true + }, + { + "id": "empty_data", + "label": "$t:editor.state.empty_data", + "sampleDataOverrides": { + "byEndpointPattern": { + "/api/modules/sirsoft-ecommerce/admin/promotion-coupons*": { + "data": { + "data": [], + "meta": { + "total": 0, + "per_page": 20, + "current_page": 1 + } + } + } + } + } + } + ] + }, + { + "comment": "admin 쿠폰 생성 — promotion-coupon-create 라우트(route.id 없음 = 신규 작성 폼). 쿠폰 관리. 검증 오류는 _local.errors.", + "scope": { + "kind": "route", + "match": "*/admin/ecommerce/promotion-coupon-create" + }, + "items": [ + { + "id": "create_mode", + "label": "$t:editor.state.create_mode", + "default": true, + "initialState": { + "route": { + "id": null + } + } + }, + { + "id": "validation_failed", + "label": "$t:editor.state.validation_failed", + "formErrors": { + "_local.errors.name": [ + "필수 입력 항목입니다" + ] + } + } + ] + }, + { + "comment": "admin 주문 목록 — orders 데이터소스 목록 유무.", + "scope": { + "kind": "route", + "match": "*/admin/ecommerce/orders" + }, + "items": [ + { + "id": "with_data", + "label": "$t:editor.state.with_data", + "default": true + }, + { + "id": "empty_data", + "label": "$t:editor.state.empty_data", + "sampleDataOverrides": { + "byEndpointPattern": { + "/api/modules/sirsoft-ecommerce/admin/orders*": { + "data": { + "data": [], + "meta": { + "total": 0, + "per_page": 20, + "current_page": 1 + } + } + } + } + } + } + ] + }, + { + "comment": "admin 주문 상세 — order 데이터소스 + 부분취소 분기(본문 if order.data.is_partially_cancelled). 주문 항목상태 전이.", + "scope": { + "kind": "route", + "match": "*/admin/ecommerce/orders/:orderNumber" + }, + "items": [ + { + "id": "default", + "label": "$t:editor.state.order_detail_default", + "default": true + }, + { + "id": "partially_cancelled", + "label": "$t:editor.state.order_detail_partially_cancelled", + "comment": "부분취소 상태 — order.data.is_partially_cancelled=true 로 취소 표시 분기를 캔버스에 노출.", + "sampleDataOverrides": { + "byEndpointPattern": { + "/api/modules/sirsoft-ecommerce/admin/orders/*": { + "data": { + "is_partially_cancelled": true + } + } + } + } + } + ] + } + ] + } + } + } +} +``` **에러 응답** diff --git a/docs/backend/api/notification-channels.md b/docs/backend/api/notification-channels.md index f761bd7e..0ca154fb 100644 --- a/docs/backend/api/notification-channels.md +++ b/docs/backend/api/notification-channels.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/notification-channels HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -35,6 +44,55 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | channels | array | `[{"id":"mail","name_key":"notification.channels.mail.name…` | 사용 가능한 알림 채널 메타데이터 목록. 각 원소는 `id`(채널 식별자: mail, database 등), `name`/`name_key`·`description`/`description_key`(활성 locale 기준 해석된 라벨/설명과 원본 다국어 키), `icon`(Font Awesome 클래스), `source`(제공 주체: core/module/plugin)·`source_label`(출처 표시 라벨), `allow_guest`(비회원 발송 허용 여부), `readiness`(컨트롤러가 `ChannelReadinessCheckerInterface`로 붙인 채널 설정 완료 여부 정보)로 구성됩니다. config 기본 채널(mail, database)에 `core.notification.filter_available_channels` 훅으로 추가된 확장 채널이 병합됩니다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "알림 채널 목록을 조회했습니다.", + "data": { + "channels": [ + { + "id": "mail", + "name_key": "notification.channels.mail.name", + "icon": "fas fa-envelope", + "description_key": "notification.channels.mail.description", + "source": "core", + "source_label_key": "notification.channels.core_default", + "allow_guest": true, + "name": "메일", + "description": "이메일로 알림 발송", + "source_label": "코어 기본 채널", + "readiness": { + "ready": false, + "reason": "notification.readiness.mail_smtp_host_empty" + } + }, + { + "id": "database", + "name_key": "notification.channels.database.name", + "icon": "fas fa-bell", + "description_key": "notification.channels.database.description", + "source": "core", + "source_label_key": "notification.channels.core_default", + "allow_guest": false, + "name": "사이트내 알림", + "description": "사이트내 알림 센터에 표시", + "source_label": "코어 기본 채널", + "readiness": { + "ready": true, + "reason": null + } + } + ] + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/notification-definitions.md b/docs/backend/api/notification-definitions.md index a7a2acc9..ef7bb9b1 100644 --- a/docs/backend/api/notification-definitions.md +++ b/docs/backend/api/notification-definitions.md @@ -38,6 +38,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification_definition.filter_index_rules`). +**요청 예시** + +```http +GET /api/admin/notification-definitions?search=%EC%98%88%EC%8B%9C%EA%B0%92&extension_type=core&extension_identifier=example-key&channel=%EC%98%88%EC%8B%9C%EA%B0%92&is_active=1&per_page=1&sort_by=id&sort_order=asc HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -62,6 +71,222 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | updated_at | string | `2026-06-30 13:33:16` | 최종 수정 일시 | | abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "알림 정의 목록을 조회했습니다.", + "data": { + "data": [ + { + "number": 1, + "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 10:41:24", + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "number": 2, + "id": 2, + "type": "order_confirmed", + "hook_prefix": "sirsoft-ecommerce", + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "name": { + "ko": "결제 완료", + "en": "Payment Completed" + }, + "description": { + "ko": "결제 완료 시 고객에게 발송", + "en": "Sent to customer when payment is completed" + }, + "variables": [ + { + "key": "name", + "description": "수신자 이름" + }, + { + "key": "app_name", + "description": "사이트 이름" + }, + { + "key": "order_number", + "description": "주문번호" + }, + { + "key": "total_amount", + "description": "결제 금액" + }, + { + "key": "order_url", + "description": "주문 상세 URL" + }, + { + "key": "site_url", + "description": "사이트 URL" + }, + { + "key": "shipping_recipient_name", + "description": "수취인 이름" + }, + { + "key": "shipping_country_name", + "description": "배송국가명" + }, + { + "key": "shipping_address", + "description": "배송지 주소" + } + ], + "channels": [ + "mail", + "database" + ], + "hooks": [ + "sirsoft-ecommerce.order.after_confirm" + ], + "is_active": true, + "is_default": true, + "templates": [ + { + "id": 2, + "definition_id": 2, + "channel": "mail", + "subject": { + "ko": "[{app_name}] 결제가 완료되었습니다 (주문번호: {order_number})", + "en": "[{app_name}] Your payment has been completed (Order #{order_number})" + }, + "body": { + "ko": "

결제 완료

{name}님, 결제가 완료되었습니다. 주문해 주셔서 감사합니다.

주문번호: {order_number}

결제금액: {total_amount}

받는분: {shipping_recipient_name}

배송국가: {shipping_country_name}

배송지: {shipping_address}

주문 상세 내용은 아래 버튼을 클릭하여 확인하실 수 있습니다.


본 메일은 {app_name}에서 발송되었습니다.

", + "en": "

Payment Completed

Dear {name}, your payment has been completed. Thank you for your order.

Order Number: {order_number}

Total Amount: {total_amount}

Recipient: {shipping_recipient_name}

Shipping Country: {shipping_country_name}

Shipping Address: {shipping_address}

Click the button below to view your order details.


This email was sent from {app_name}.

" + }, + "click_url": null, + "recipients": [ + { + "type": "trigger_user" + } + ], + "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 + } + }, + { + "id": 3, + "definition_id": 2, + "channel": "database", + "subject": { + "ko": "결제가 완료되었습니다", + "en": "Your payment has been completed" + }, + "body": { + "ko": "{name}님, 주문번호 {order_number}의 결제가 완료되었습니다.", + "en": "{name}, payment for your order {order_number} has been completed." + }, + "click_url": "{order_url}", + "recipients": [ + { + "type": "trigger_user" + } + ], + "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 10:43:32", + "abilities": { + "can_update": true, + "can_delete": true + } + }, + "... (총 18건 중 2건 표시)" + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 18, + "from": 1, + "to": 18, + "has_more_pages": false + }, + "abilities": { + "can_update": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -87,6 +312,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | definition | path | string | 예 | — | 대상 definition의 식별자 | +**요청 예시** + +```http +GET /api/admin/notification-definitions/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -110,6 +344,75 @@ _단건 응답: `data` 객체의 필드._ | updated_at | string | `2026-07-06 19:15:16` | 최종 수정 일시 | | abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```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 10:41:24", + "abilities": { + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -140,10 +443,34 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification_definition.filter_update_rules`). +**요청 예시** + +```http +PUT /api/admin/notification-definitions/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "channels": [ + "예시값" + ], + "hooks": [ + "예시값" + ], + "is_active": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -170,9 +497,106 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | definition | path | string | 예 | — | 대상 definition의 식별자 | +**요청 예시** + +```http +POST /api/admin/notification-definitions/1/reset HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| 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/template) | +| 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 | `true` | default 여부 | +| templates | array | `[{"id":1,"definition_id":1,"channel":"mail","subject":"AP…` | 템플릿 목록 (각 원소 identifier/name 등 — 템플릿 관계 파생) | +| 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 등 — 권한 맵 기반) | + +**응답 예시** + +```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": true, + "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 + } + } +} +``` **에러 응답** @@ -199,9 +623,80 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | definition | path | string | 예 | — | 대상 definition의 식별자 | +**요청 예시** + +```http +PATCH /api/admin/notification-definitions/1/toggle-active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| 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/template) | +| 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 | `false` | active 여부 | +| is_default | boolean | `false` | default 여부 | +| templates | null | `null` | 템플릿 목록 (각 원소 identifier/name 등 — 템플릿 관계 파생) | +| 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 등 — 권한 맵 기반) | + +**응답 예시** + +```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": false, + "is_default": false, + "templates": null, + "created_at": "2026-07-08 10:41:24", + "updated_at": "2026-07-08 12:14:43", + "abilities": { + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** diff --git a/docs/backend/api/notification-logs.md b/docs/backend/api/notification-logs.md index dcf9dcb1..30e2374f 100644 --- a/docs/backend/api/notification-logs.md +++ b/docs/backend/api/notification-logs.md @@ -40,6 +40,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification_log.filter_index_rules`). +**요청 예시** + +```http +GET /api/admin/notification-logs?sender_user_id=1&recipient_user_id=1&search=%EC%98%88%EC%8B%9C%EA%B0%92&channel=%EC%98%88%EC%8B%9C%EA%B0%92¬ification_type=%EC%98%88%EC%8B%9C%EA%B0%92&extension_type=core&status=%EC%98%88%EC%8B%9C%EA%B0%92&per_page=1&sort_by=id&sort_order=asc HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -68,6 +77,68 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | updated_at | string | `2026-07-06 19:20:23` | 최종 수정 일시 | | abilities | object | `{"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "알림 발송 이력을 조회했습니다.", + "data": { + "data": [ + { + "number": 1, + "id": 1, + "channel": "mail", + "notification_type": "apidoc.sample.event", + "extension_type": "core", + "extension_identifier": "", + "recipient_user_id": 6, + "recipient_identifier": "apidoc-sample-user@example.com", + "recipient_name": "API 문서 샘플 사용자", + "sender_user_id": 6, + "sender": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" + }, + "recipient": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" + }, + "subject": "API 문서 샘플 알림", + "body": "문서 실측용 알림 본문입니다.", + "status": "sent", + "error_message": null, + "source": "apidoc", + "sent_at": "2026-07-08 09:41:24", + "created_at": "2026-07-08 10:41:24", + "updated_at": "2026-07-08 10:41:24", + "abilities": { + "can_delete": true + } + } + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + }, + "abilities": { + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -93,10 +164,30 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +**요청 예시** + +```http +POST /api/admin/notification-logs/bulk-delete HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -122,9 +213,34 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | notificationLog | path | string | 예 | — | 대상 notification log의 식별자 | +**요청 예시** + +```http +DELETE /api/admin/notification-logs/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "알림 발송 이력이 삭제되었습니다.", + "data": null +} +``` **에러 응답** diff --git a/docs/backend/api/notification-templates.md b/docs/backend/api/notification-templates.md index da70ef44..ae3cc060 100644 --- a/docs/backend/api/notification-templates.md +++ b/docs/backend/api/notification-templates.md @@ -32,10 +32,35 @@ | body | body | array | 예 | — | 본문 | | locale | body | string | 아니오 | max 10 | 로케일 코드 (표시 언어/지역) | +**요청 예시** + +```http +POST /api/admin/notification-templates/preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "definition_id": 1, + "subject": [ + "예시값" + ], + "body": [ + "예시값" + ], + "locale": "ko" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -68,10 +93,38 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification_template.filter_update_rules`). +**요청 예시** + +```http +PUT /api/admin/notification-templates/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "subject": [ + "예시값" + ], + "body": [ + "예시값" + ], + "click_url": "https://example.com", + "recipients": [ + "예시값" + ], + "is_active": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -98,10 +151,23 @@ | --- | --- | --- | --- | --- | --- | | template | path | string | 예 | — | 대상 template의 식별자 | +**요청 예시** + +```http +POST /api/admin/notification-templates/1/reset HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -127,9 +193,75 @@ | --- | --- | --- | --- | --- | --- | | template | path | string | 예 | — | 대상 template의 식별자 | +**요청 예시** + +```http +PATCH /api/admin/notification-templates/1/toggle-active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| definition_id | integer | `1` | definition 식별자 (연관 리소스 참조) | +| channel | string | `mail` | 채널: mail, database, fcm | +| subject | string | `API 문서 샘플 템플릿 제목` | 다국어 제목 ({"ko": "...", "en": "..."}) | +| body | string | `안녕하세요 {{name}} 님, 문서 실측용 본문입니다.` | 다국어 본문 ({"ko": "...", "en": "..."}) | +| click_url | string | `/admin/apidoc-sample` | click URL | +| recipients | array | `[{"type":"role","value":"admin","display_name":"관리자"}]` | 수신자 규칙 JSON ([{type, value, relation, exclude_trigger_user}]) | +| is_active | boolean | `false` | active 여부 | +| is_default | boolean | `false` | default 여부 | +| user_overrides | array | `["is_active"]` | 사용자가 수정한 필드명 목록 | +| updated_by | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 최종 수정한 사용자 정보 (uuid/name — updated_by 관계 파생, 없으면 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "알림 템플릿 활성 상태가 변경되었습니다.", + "data": { + "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": false, + "is_default": false, + "user_overrides": [ + "is_active" + ], + "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 + } + } +} +``` **에러 응답** diff --git a/docs/backend/api/notifications.md b/docs/backend/api/notifications.md index 42c0855c..5d178abd 100644 --- a/docs/backend/api/notifications.md +++ b/docs/backend/api/notifications.md @@ -34,12 +34,46 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification.filter_index_rules`). +**요청 예시** + +```http +GET /api/admin/notifications?read=unread&per_page=1&page=1&sort_order=asc HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "알림 목록을 조회했습니다.", + "data": { + "data": [], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 0, + "from": null, + "to": null, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -65,9 +99,38 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ _요청 파라미터 없음._ +**요청 예시** + +```http +DELETE /api/admin/notifications/all HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| deleted_count | integer | `0` | deleted 개수 (집계) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "모든 알림이 삭제되었습니다.", + "data": { + "deleted_count": 0 + } +} +``` **에러 응답** @@ -93,9 +156,38 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/notifications/read-all HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| marked_count | integer | `0` | marked 개수 (집계) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "모든 알림을 읽음 처리했습니다.", + "data": { + "marked_count": 0 + } +} +``` **에러 응답** @@ -125,10 +217,30 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification.filter_batch_read_rules`). +**요청 예시** + +```http +POST /api/admin/notifications/read-batch HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -154,6 +266,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/notifications/unread-count HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -162,6 +283,22 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | unread_count | integer | `0` | unread 개수 (집계) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "미읽음 알림 수를 조회했습니다.", + "data": { + "unread_count": 0 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -188,10 +325,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | notification | path | string | 예 | — | 대상 notification의 식별자 | +**요청 예시** + +```http +DELETE /api/admin/notifications/{notification} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -219,10 +369,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | notification | path | string | 예 | — | 대상 notification의 식별자 | +**요청 예시** + +```http +PATCH /api/admin/notifications/{notification}/read HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -255,12 +418,46 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification.filter_index_rules`). +**요청 예시** + +```http +GET /api/user/notifications?read=unread&per_page=1&page=1&sort_order=asc HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "알림 목록을 조회했습니다.", + "data": { + "data": [], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 0, + "from": null, + "to": null, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -285,9 +482,38 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ _요청 파라미터 없음._ +**요청 예시** + +```http +DELETE /api/user/notifications/all HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| deleted_count | integer | `0` | deleted 개수 (집계) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "모든 알림이 삭제되었습니다.", + "data": { + "deleted_count": 0 + } +} +``` **에러 응답** @@ -312,9 +538,38 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/user/notifications/read-all HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| marked_count | integer | `0` | marked 개수 (집계) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "모든 알림을 읽음 처리했습니다.", + "data": { + "marked_count": 0 + } +} +``` **에러 응답** @@ -343,10 +598,30 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification.filter_batch_read_rules`). +**요청 예시** + +```http +POST /api/user/notifications/read-batch HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +Content-Type: application/json + +{ + "ids": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -371,6 +646,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/user/notifications/unread-count HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -379,6 +663,22 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | unread_count | integer | `0` | unread 개수 (집계) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "미읽음 알림 수를 조회했습니다.", + "data": { + "unread_count": 0 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -404,10 +704,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | notification | path | string | 예 | — | 대상 notification의 식별자 | +**요청 예시** + +```http +DELETE /api/user/notifications/{notification} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -434,10 +747,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | notification | path | string | 예 | — | 대상 notification의 식별자 | +**요청 예시** + +```http +PATCH /api/user/notifications/{notification}/read HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/password.md b/docs/backend/api/password.md index 5de1eb60..ef99e7f7 100644 --- a/docs/backend/api/password.md +++ b/docs/backend/api/password.md @@ -33,10 +33,30 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.change_password_validation_rules`). +**요청 예시** + +```http +PUT /api/me/password HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "current_password": "Password123!", + "password": "Password123!", + "password_confirmation": "Password123!" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/permissions.md b/docs/backend/api/permissions.md index 36bc7714..48a23900 100644 --- a/docs/backend/api/permissions.md +++ b/docs/backend/api/permissions.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/permissions HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -36,6 +45,4538 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | admin | object | `{"label":"관리자 권한","icon":"cog","permissions":[{"id":1,"id…` | 관리자(admin) 타입 권한 그룹. `label`·`icon`(PermissionType::Admin 의 label()/icon() 산물)과 admin 타입으로 필터링된 권한 트리(`permissions`)를 담는다. | | user | object | `{"label":"사용자 권한","icon":"user","permissions":[{"id":1,"i…` | 사용자(user) 타입 권한 그룹. `label`·`icon`(PermissionType::User 의 label()/icon() 산물)과 user 타입으로 필터링된 권한 트리(`permissions`)를 담는다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "권한 정보를 성공적으로 가져왔습니다.", + "data": { + "data": { + "permissions": { + "admin": { + "label": "관리자 권한", + "icon": "cog", + "permissions": [ + { + "id": 115, + "identifier": "core", + "name": "코어", + "name_raw": { + "ko": "코어", + "en": "Core" + }, + "description": "코어 시스템 권한", + "description_raw": { + "ko": "코어 시스템 권한", + "en": "Core system permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 116, + "identifier": "core.users", + "name": "사용자 관리", + "name_raw": { + "ko": "사용자 관리", + "en": "User Management" + }, + "description": "사용자 관리 권한", + "description_raw": { + "ko": "사용자 관리 권한", + "en": "User management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 117, + "identifier": "core.users.read", + "name": "사용자 조회", + "name_raw": { + "ko": "사용자 조회", + "en": "View Users" + }, + "description": "사용자 목록 및 상세 정보를 조회할 수 있습니다.", + "description_raw": { + "ko": "사용자 목록 및 상세 정보를 조회할 수 있습니다.", + "en": "Can view user list and details." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": "user", + "owner_key": "id", + "children": [], + "leaf_count": 1 + }, + { + "id": 118, + "identifier": "core.users.create", + "name": "사용자 생성", + "name_raw": { + "ko": "사용자 생성", + "en": "Create Users" + }, + "description": "새로운 사용자를 생성할 수 있습니다.", + "description_raw": { + "ko": "새로운 사용자를 생성할 수 있습니다.", + "en": "Can create new users." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 119, + "identifier": "core.users.update", + "name": "사용자 수정", + "name_raw": { + "ko": "사용자 수정", + "en": "Update Users" + }, + "description": "사용자 정보를 수정할 수 있습니다.", + "description_raw": { + "ko": "사용자 정보를 수정할 수 있습니다.", + "en": "Can update user information." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": "user", + "owner_key": "id", + "children": [], + "leaf_count": 1 + }, + { + "id": 120, + "identifier": "core.users.delete", + "name": "사용자 삭제", + "name_raw": { + "ko": "사용자 삭제", + "en": "Delete Users" + }, + "description": "사용자를 삭제할 수 있습니다.", + "description_raw": { + "ko": "사용자를 삭제할 수 있습니다.", + "en": "Can delete users." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": "user", + "owner_key": "id", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + }, + { + "id": 121, + "identifier": "core.menus", + "name": "메뉴 관리", + "name_raw": { + "ko": "메뉴 관리", + "en": "Menu Management" + }, + "description": "메뉴 관리 권한", + "description_raw": { + "ko": "메뉴 관리 권한", + "en": "Menu management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 122, + "identifier": "core.menus.read", + "name": "메뉴 조회", + "name_raw": { + "ko": "메뉴 조회", + "en": "View Menus" + }, + "description": "메뉴 목록을 조회할 수 있습니다.", + "description_raw": { + "ko": "메뉴 목록을 조회할 수 있습니다.", + "en": "Can view menu list." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": "menu", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 123, + "identifier": "core.menus.create", + "name": "메뉴 생성", + "name_raw": { + "ko": "메뉴 생성", + "en": "Create Menus" + }, + "description": "새로운 메뉴를 생성할 수 있습니다.", + "description_raw": { + "ko": "새로운 메뉴를 생성할 수 있습니다.", + "en": "Can create new menus." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 124, + "identifier": "core.menus.update", + "name": "메뉴 수정", + "name_raw": { + "ko": "메뉴 수정", + "en": "Update Menus" + }, + "description": "메뉴 정보를 수정할 수 있습니다.", + "description_raw": { + "ko": "메뉴 정보를 수정할 수 있습니다.", + "en": "Can update menu information." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": "menu", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 125, + "identifier": "core.menus.delete", + "name": "메뉴 삭제", + "name_raw": { + "ko": "메뉴 삭제", + "en": "Delete Menus" + }, + "description": "메뉴를 삭제할 수 있습니다.", + "description_raw": { + "ko": "메뉴를 삭제할 수 있습니다.", + "en": "Can delete menus." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": "menu", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + }, + { + "id": 126, + "identifier": "core.modules", + "name": "모듈 관리", + "name_raw": { + "ko": "모듈 관리", + "en": "Module Management" + }, + "description": "모듈 관리 권한", + "description_raw": { + "ko": "모듈 관리 권한", + "en": "Module management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 127, + "identifier": "core.modules.read", + "name": "모듈 조회", + "name_raw": { + "ko": "모듈 조회", + "en": "View Modules" + }, + "description": "모듈 목록을 조회할 수 있습니다.", + "description_raw": { + "ko": "모듈 목록을 조회할 수 있습니다.", + "en": "Can view module list." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 128, + "identifier": "core.modules.install", + "name": "모듈 설치", + "name_raw": { + "ko": "모듈 설치", + "en": "Install Modules" + }, + "description": "새로운 모듈을 설치할 수 있습니다.", + "description_raw": { + "ko": "새로운 모듈을 설치할 수 있습니다.", + "en": "Can install new modules." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 129, + "identifier": "core.modules.activate", + "name": "모듈 활성화", + "name_raw": { + "ko": "모듈 활성화", + "en": "Activate Modules" + }, + "description": "모듈을 활성화/비활성화할 수 있습니다.", + "description_raw": { + "ko": "모듈을 활성화/비활성화할 수 있습니다.", + "en": "Can activate/deactivate modules." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 130, + "identifier": "core.modules.uninstall", + "name": "모듈 삭제", + "name_raw": { + "ko": "모듈 삭제", + "en": "Uninstall Modules" + }, + "description": "모듈을 삭제할 수 있습니다.", + "description_raw": { + "ko": "모듈을 삭제할 수 있습니다.", + "en": "Can uninstall modules." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + }, + { + "id": 131, + "identifier": "core.plugins", + "name": "플러그인 관리", + "name_raw": { + "ko": "플러그인 관리", + "en": "Plugin Management" + }, + "description": "플러그인 관리 권한", + "description_raw": { + "ko": "플러그인 관리 권한", + "en": "Plugin management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 132, + "identifier": "core.plugins.read", + "name": "플러그인 조회", + "name_raw": { + "ko": "플러그인 조회", + "en": "View Plugins" + }, + "description": "플러그인 목록을 조회할 수 있습니다.", + "description_raw": { + "ko": "플러그인 목록을 조회할 수 있습니다.", + "en": "Can view plugin list." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 133, + "identifier": "core.plugins.install", + "name": "플러그인 설치", + "name_raw": { + "ko": "플러그인 설치", + "en": "Install Plugins" + }, + "description": "새로운 플러그인을 설치할 수 있습니다.", + "description_raw": { + "ko": "새로운 플러그인을 설치할 수 있습니다.", + "en": "Can install new plugins." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 134, + "identifier": "core.plugins.activate", + "name": "플러그인 활성화", + "name_raw": { + "ko": "플러그인 활성화", + "en": "Activate Plugins" + }, + "description": "플러그인을 활성화/비활성화할 수 있습니다.", + "description_raw": { + "ko": "플러그인을 활성화/비활성화할 수 있습니다.", + "en": "Can activate/deactivate plugins." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 135, + "identifier": "core.plugins.update", + "name": "플러그인 설정", + "name_raw": { + "ko": "플러그인 설정", + "en": "Configure Plugins" + }, + "description": "플러그인 환경설정을 수정할 수 있습니다.", + "description_raw": { + "ko": "플러그인 환경설정을 수정할 수 있습니다.", + "en": "Can update plugin settings." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 136, + "identifier": "core.plugins.uninstall", + "name": "플러그인 삭제", + "name_raw": { + "ko": "플러그인 삭제", + "en": "Uninstall Plugins" + }, + "description": "플러그인을 삭제할 수 있습니다.", + "description_raw": { + "ko": "플러그인을 삭제할 수 있습니다.", + "en": "Can uninstall plugins." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 5 + }, + { + "id": 137, + "identifier": "core.templates", + "name": "템플릿 관리", + "name_raw": { + "ko": "템플릿 관리", + "en": "Template Management" + }, + "description": "템플릿 관리 권한", + "description_raw": { + "ko": "템플릿 관리 권한", + "en": "Template management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 138, + "identifier": "core.templates.read", + "name": "템플릿 조회", + "name_raw": { + "ko": "템플릿 조회", + "en": "View Templates" + }, + "description": "템플릿 목록을 조회할 수 있습니다.", + "description_raw": { + "ko": "템플릿 목록을 조회할 수 있습니다.", + "en": "Can view template list." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 139, + "identifier": "core.templates.install", + "name": "템플릿 설치", + "name_raw": { + "ko": "템플릿 설치", + "en": "Install Templates" + }, + "description": "새로운 템플릿을 설치할 수 있습니다.", + "description_raw": { + "ko": "새로운 템플릿을 설치할 수 있습니다.", + "en": "Can install new templates." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 140, + "identifier": "core.templates.activate", + "name": "템플릿 활성화", + "name_raw": { + "ko": "템플릿 활성화", + "en": "Activate Templates" + }, + "description": "템플릿을 활성화/비활성화할 수 있습니다.", + "description_raw": { + "ko": "템플릿을 활성화/비활성화할 수 있습니다.", + "en": "Can activate/deactivate templates." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 141, + "identifier": "core.templates.uninstall", + "name": "템플릿 삭제", + "name_raw": { + "ko": "템플릿 삭제", + "en": "Uninstall Templates" + }, + "description": "템플릿을 삭제할 수 있습니다.", + "description_raw": { + "ko": "템플릿을 삭제할 수 있습니다.", + "en": "Can uninstall templates." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 142, + "identifier": "core.templates.layouts.edit", + "name": "레이아웃 편집", + "name_raw": { + "ko": "레이아웃 편집", + "en": "Edit Layouts" + }, + "description": "템플릿 레이아웃을 편집할 수 있습니다.", + "description_raw": { + "ko": "템플릿 레이아웃을 편집할 수 있습니다.", + "en": "Can edit template layouts." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 5 + }, + { + "id": 143, + "identifier": "core.permissions", + "name": "권한 관리", + "name_raw": { + "ko": "권한 관리", + "en": "Permission Management" + }, + "description": "역할 및 권한 관리 권한", + "description_raw": { + "ko": "역할 및 권한 관리 권한", + "en": "Role and permission management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 144, + "identifier": "core.permissions.read", + "name": "권한 조회", + "name_raw": { + "ko": "권한 조회", + "en": "View Permissions" + }, + "description": "역할 및 권한 목록을 조회할 수 있습니다.", + "description_raw": { + "ko": "역할 및 권한 목록을 조회할 수 있습니다.", + "en": "Can view roles and permissions." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 145, + "identifier": "core.permissions.create", + "name": "역할 생성", + "name_raw": { + "ko": "역할 생성", + "en": "Create Roles" + }, + "description": "새로운 역할을 생성할 수 있습니다.", + "description_raw": { + "ko": "새로운 역할을 생성할 수 있습니다.", + "en": "Can create new roles." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 146, + "identifier": "core.permissions.update", + "name": "역할 수정", + "name_raw": { + "ko": "역할 수정", + "en": "Update Roles" + }, + "description": "역할 정보와 권한을 수정할 수 있습니다.", + "description_raw": { + "ko": "역할 정보와 권한을 수정할 수 있습니다.", + "en": "Can update role information and permissions." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 147, + "identifier": "core.permissions.delete", + "name": "역할 삭제", + "name_raw": { + "ko": "역할 삭제", + "en": "Delete Roles" + }, + "description": "역할을 삭제할 수 있습니다.", + "description_raw": { + "ko": "역할을 삭제할 수 있습니다.", + "en": "Can delete roles." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + }, + { + "id": 148, + "identifier": "core.notification-logs", + "name": "알림 발송 이력", + "name_raw": { + "ko": "알림 발송 이력", + "en": "Notification Logs" + }, + "description": "알림 발송 이력 관리 권한", + "description_raw": { + "ko": "알림 발송 이력 관리 권한", + "en": "Notification log management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 149, + "identifier": "core.notification-logs.read", + "name": "발송 이력 조회", + "name_raw": { + "ko": "발송 이력 조회", + "en": "View Notification Logs" + }, + "description": "알림 발송 이력을 조회할 수 있습니다.", + "description_raw": { + "ko": "알림 발송 이력을 조회할 수 있습니다.", + "en": "Can view notification logs." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 150, + "identifier": "core.notification-logs.delete", + "name": "발송 이력 삭제", + "name_raw": { + "ko": "발송 이력 삭제", + "en": "Delete Notification Logs" + }, + "description": "알림 발송 이력을 삭제할 수 있습니다.", + "description_raw": { + "ko": "알림 발송 이력을 삭제할 수 있습니다.", + "en": "Can delete notification logs." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 2 + }, + { + "id": 151, + "identifier": "core.notifications", + "name": "알림 (관리자)", + "name_raw": { + "ko": "알림 (관리자)", + "en": "Notifications (Admin)" + }, + "description": "관리자용 알림 관리 권한 (관리자 화면에서 사용)", + "description_raw": { + "ko": "관리자용 알림 관리 권한 (관리자 화면에서 사용)", + "en": "Admin notification management permissions (used in admin UI)" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 152, + "identifier": "core.notifications.read", + "name": "알림 조회", + "name_raw": { + "ko": "알림 조회", + "en": "View Notifications" + }, + "description": "알림 목록 및 읽지 않은 수를 조회할 수 있습니다.", + "description_raw": { + "ko": "알림 목록 및 읽지 않은 수를 조회할 수 있습니다.", + "en": "Can view notification list and unread count." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 153, + "identifier": "core.notifications.update", + "name": "알림 읽음 처리", + "name_raw": { + "ko": "알림 읽음 처리", + "en": "Mark Notifications Read" + }, + "description": "알림을 읽음 처리할 수 있습니다.", + "description_raw": { + "ko": "알림을 읽음 처리할 수 있습니다.", + "en": "Can mark notifications as read." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 154, + "identifier": "core.notifications.delete", + "name": "알림 삭제", + "name_raw": { + "ko": "알림 삭제", + "en": "Delete Notifications" + }, + "description": "알림을 삭제할 수 있습니다.", + "description_raw": { + "ko": "알림을 삭제할 수 있습니다.", + "en": "Can delete notifications." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 3 + }, + { + "id": 163, + "identifier": "core.admin.identity", + "name": "본인인증 관리 (관리자)", + "name_raw": { + "ko": "본인인증 관리 (관리자)", + "en": "Identity Verification Management (Admin)" + }, + "description": "관리자용 IDV 프로바이더/정책/로그 관리 권한", + "description_raw": { + "ko": "관리자용 IDV 프로바이더/정책/로그 관리 권한", + "en": "Admin permissions for IDV providers, policies, and logs" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 164, + "identifier": "core.admin.identity.providers.read", + "name": "프로바이더 설정 조회", + "name_raw": { + "ko": "프로바이더 설정 조회", + "en": "View IDV Providers" + }, + "description": "본인인증 프로바이더 설정을 조회할 수 있습니다.", + "description_raw": { + "ko": "본인인증 프로바이더 설정을 조회할 수 있습니다.", + "en": "Can view identity verification providers." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 165, + "identifier": "core.admin.identity.providers.update", + "name": "프로바이더 설정 수정", + "name_raw": { + "ko": "프로바이더 설정 수정", + "en": "Update IDV Providers" + }, + "description": "본인인증 프로바이더 설정을 수정할 수 있습니다.", + "description_raw": { + "ko": "본인인증 프로바이더 설정을 수정할 수 있습니다.", + "en": "Can update identity verification providers." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 166, + "identifier": "core.admin.identity.policies.read", + "name": "정책 조회", + "name_raw": { + "ko": "정책 조회", + "en": "View IDV Policies" + }, + "description": "본인인증 정책(라우트/훅별)을 조회할 수 있습니다.", + "description_raw": { + "ko": "본인인증 정책(라우트/훅별)을 조회할 수 있습니다.", + "en": "Can view identity verification policies by route/hook." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 167, + "identifier": "core.admin.identity.policies.update", + "name": "정책 수정", + "name_raw": { + "ko": "정책 수정", + "en": "Update IDV Policies" + }, + "description": "본인인증 정책(라우트/훅별)을 CRUD 할 수 있습니다.", + "description_raw": { + "ko": "본인인증 정책(라우트/훅별)을 CRUD 할 수 있습니다.", + "en": "Can CRUD identity verification policies by route/hook." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 168, + "identifier": "core.admin.identity.logs.read", + "name": "로그 열람", + "name_raw": { + "ko": "로그 열람", + "en": "View IDV Logs" + }, + "description": "본인인증 이력을 조회할 수 있습니다.", + "description_raw": { + "ko": "본인인증 이력을 조회할 수 있습니다.", + "en": "Can view identity verification logs." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 169, + "identifier": "core.admin.identity.logs.purge", + "name": "로그 파기", + "name_raw": { + "ko": "로그 파기", + "en": "Purge IDV Logs" + }, + "description": "본인인증 이력을 파기(보관주기 외)할 수 있습니다.", + "description_raw": { + "ko": "본인인증 이력을 파기(보관주기 외)할 수 있습니다.", + "en": "Can purge identity verification logs (retention-based)." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 170, + "identifier": "core.admin.identity.messages.read", + "name": "메시지 템플릿 조회", + "name_raw": { + "ko": "메시지 템플릿 조회", + "en": "View IDV Message Templates" + }, + "description": "본인인증 메시지 정의/템플릿을 조회할 수 있습니다.", + "description_raw": { + "ko": "본인인증 메시지 정의/템플릿을 조회할 수 있습니다.", + "en": "Can view identity verification message definitions and templates." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 171, + "identifier": "core.admin.identity.messages.update", + "name": "메시지 템플릿 수정", + "name_raw": { + "ko": "메시지 템플릿 수정", + "en": "Update IDV Message Templates" + }, + "description": "본인인증 메시지 정의/템플릿을 수정할 수 있습니다.", + "description_raw": { + "ko": "본인인증 메시지 정의/템플릿을 수정할 수 있습니다.", + "en": "Can update identity verification message definitions and templates." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 8 + }, + { + "id": 172, + "identifier": "core.settings", + "name": "환경설정", + "name_raw": { + "ko": "환경설정", + "en": "Settings" + }, + "description": "시스템 환경설정 권한", + "description_raw": { + "ko": "시스템 환경설정 권한", + "en": "System settings permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 173, + "identifier": "core.settings.read", + "name": "설정 조회", + "name_raw": { + "ko": "설정 조회", + "en": "View Settings" + }, + "description": "시스템 설정을 조회할 수 있습니다.", + "description_raw": { + "ko": "시스템 설정을 조회할 수 있습니다.", + "en": "Can view system settings." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 174, + "identifier": "core.settings.update", + "name": "설정 수정", + "name_raw": { + "ko": "설정 수정", + "en": "Update Settings" + }, + "description": "시스템 설정을 수정할 수 있습니다.", + "description_raw": { + "ko": "시스템 설정을 수정할 수 있습니다.", + "en": "Can update system settings." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 2 + }, + { + "id": 175, + "identifier": "core.dashboard", + "name": "대시보드", + "name_raw": { + "ko": "대시보드", + "en": "Dashboard" + }, + "description": "대시보드 접근 권한", + "description_raw": { + "ko": "대시보드 접근 권한", + "en": "Dashboard access permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 176, + "identifier": "core.dashboard.read", + "name": "대시보드 조회", + "name_raw": { + "ko": "대시보드 조회", + "en": "View Dashboard" + }, + "description": "대시보드 통계 및 정보를 조회할 수 있습니다.", + "description_raw": { + "ko": "대시보드 통계 및 정보를 조회할 수 있습니다.", + "en": "Can view dashboard statistics and information." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 177, + "identifier": "core.dashboard.system-status", + "name": "시스템 상태", + "name_raw": { + "ko": "시스템 상태", + "en": "System Status" + }, + "description": "시스템 상태 정보를 조회할 수 있습니다.", + "description_raw": { + "ko": "시스템 상태 정보를 조회할 수 있습니다.", + "en": "Can view system status information." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 178, + "identifier": "core.dashboard.resources", + "name": "시스템 리소스", + "name_raw": { + "ko": "시스템 리소스", + "en": "System Resources" + }, + "description": "CPU, 메모리, 디스크 사용량을 조회할 수 있습니다.", + "description_raw": { + "ko": "CPU, 메모리, 디스크 사용량을 조회할 수 있습니다.", + "en": "Can view CPU, memory, and disk usage." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 179, + "identifier": "core.dashboard.activities", + "name": "최근 활동", + "name_raw": { + "ko": "최근 활동", + "en": "Recent Activities" + }, + "description": "최근 활동 이력을 조회할 수 있습니다.", + "description_raw": { + "ko": "최근 활동 이력을 조회할 수 있습니다.", + "en": "Can view recent activity history." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": "activityLog", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 180, + "identifier": "core.dashboard.alerts", + "name": "시스템 알림", + "name_raw": { + "ko": "시스템 알림", + "en": "System Alerts" + }, + "description": "시스템 알림을 조회할 수 있습니다.", + "description_raw": { + "ko": "시스템 알림을 조회할 수 있습니다.", + "en": "Can view system alerts." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 5 + }, + { + "id": 181, + "identifier": "core.activities", + "name": "활동 로그", + "name_raw": { + "ko": "활동 로그", + "en": "Activity Logs" + }, + "description": "활동 로그 조회 권한", + "description_raw": { + "ko": "활동 로그 조회 권한", + "en": "Activity log access permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 182, + "identifier": "core.activities.read", + "name": "활동 로그 조회", + "name_raw": { + "ko": "활동 로그 조회", + "en": "View Activity Logs" + }, + "description": "활동 로그를 조회할 수 있습니다.", + "description_raw": { + "ko": "활동 로그를 조회할 수 있습니다.", + "en": "Can view activity logs." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": "activityLog", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 183, + "identifier": "core.activities.delete", + "name": "활동 로그 삭제", + "name_raw": { + "ko": "활동 로그 삭제", + "en": "Delete Activity Logs" + }, + "description": "활동 로그를 삭제할 수 있습니다.", + "description_raw": { + "ko": "활동 로그를 삭제할 수 있습니다.", + "en": "Can delete activity logs." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 2 + }, + { + "id": 184, + "identifier": "core.attachments", + "name": "첨부파일 관리", + "name_raw": { + "ko": "첨부파일 관리", + "en": "Attachment Management" + }, + "description": "첨부파일 관리 권한", + "description_raw": { + "ko": "첨부파일 관리 권한", + "en": "Attachment management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 185, + "identifier": "core.attachments.create", + "name": "첨부파일 업로드", + "name_raw": { + "ko": "첨부파일 업로드", + "en": "Upload Attachments" + }, + "description": "첨부파일을 업로드할 수 있습니다.", + "description_raw": { + "ko": "첨부파일을 업로드할 수 있습니다.", + "en": "Can upload attachments." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 186, + "identifier": "core.attachments.update", + "name": "첨부파일 수정", + "name_raw": { + "ko": "첨부파일 수정", + "en": "Update Attachments" + }, + "description": "첨부파일 정보 및 순서를 수정할 수 있습니다.", + "description_raw": { + "ko": "첨부파일 정보 및 순서를 수정할 수 있습니다.", + "en": "Can update attachment information and order." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": "attachment", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 187, + "identifier": "core.attachments.delete", + "name": "첨부파일 삭제", + "name_raw": { + "ko": "첨부파일 삭제", + "en": "Delete Attachments" + }, + "description": "첨부파일을 삭제할 수 있습니다.", + "description_raw": { + "ko": "첨부파일을 삭제할 수 있습니다.", + "en": "Can delete attachments." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": "attachment", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 3 + }, + { + "id": 188, + "identifier": "core.schedules", + "name": "스케줄 관리", + "name_raw": { + "ko": "스케줄 관리", + "en": "Schedule Management" + }, + "description": "스케줄 작업 관리 권한", + "description_raw": { + "ko": "스케줄 작업 관리 권한", + "en": "Schedule task management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 189, + "identifier": "core.schedules.read", + "name": "스케줄 조회", + "name_raw": { + "ko": "스케줄 조회", + "en": "View Schedules" + }, + "description": "스케줄 목록 및 상세 정보를 조회할 수 있습니다.", + "description_raw": { + "ko": "스케줄 목록 및 상세 정보를 조회할 수 있습니다.", + "en": "Can view schedule list and details." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 190, + "identifier": "core.schedules.create", + "name": "스케줄 생성", + "name_raw": { + "ko": "스케줄 생성", + "en": "Create Schedules" + }, + "description": "새로운 스케줄을 생성할 수 있습니다.", + "description_raw": { + "ko": "새로운 스케줄을 생성할 수 있습니다.", + "en": "Can create new schedules." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 191, + "identifier": "core.schedules.update", + "name": "스케줄 수정", + "name_raw": { + "ko": "스케줄 수정", + "en": "Update Schedules" + }, + "description": "스케줄 정보를 수정할 수 있습니다.", + "description_raw": { + "ko": "스케줄 정보를 수정할 수 있습니다.", + "en": "Can update schedule information." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 192, + "identifier": "core.schedules.delete", + "name": "스케줄 삭제", + "name_raw": { + "ko": "스케줄 삭제", + "en": "Delete Schedules" + }, + "description": "스케줄을 삭제할 수 있습니다.", + "description_raw": { + "ko": "스케줄을 삭제할 수 있습니다.", + "en": "Can delete schedules." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 193, + "identifier": "core.schedules.run", + "name": "스케줄 실행", + "name_raw": { + "ko": "스케줄 실행", + "en": "Run Schedules" + }, + "description": "스케줄을 수동으로 실행할 수 있습니다.", + "description_raw": { + "ko": "스케줄을 수동으로 실행할 수 있습니다.", + "en": "Can manually run schedules." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 5 + }, + { + "id": 194, + "identifier": "core.language_packs", + "name": "언어팩 관리", + "name_raw": { + "ko": "언어팩 관리", + "en": "Language Pack Management" + }, + "description": "언어팩 설치/제거/활성화 권한", + "description_raw": { + "ko": "언어팩 설치/제거/활성화 권한", + "en": "Language pack install/uninstall/activation permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 195, + "identifier": "core.language_packs.read", + "name": "언어팩 조회", + "name_raw": { + "ko": "언어팩 조회", + "en": "View Language Packs" + }, + "description": "설치된 언어팩 목록 및 상세 정보를 조회할 수 있습니다.", + "description_raw": { + "ko": "설치된 언어팩 목록 및 상세 정보를 조회할 수 있습니다.", + "en": "Can view installed language pack list and details." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 196, + "identifier": "core.language_packs.install", + "name": "언어팩 설치", + "name_raw": { + "ko": "언어팩 설치", + "en": "Install Language Packs" + }, + "description": "ZIP/GitHub/URL 로 언어팩을 설치할 수 있습니다.", + "description_raw": { + "ko": "ZIP/GitHub/URL 로 언어팩을 설치할 수 있습니다.", + "en": "Can install language packs from ZIP/GitHub/URL." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 197, + "identifier": "core.language_packs.manage", + "name": "언어팩 관리", + "name_raw": { + "ko": "언어팩 관리", + "en": "Manage Language Packs" + }, + "description": "언어팩을 활성화/비활성화/제거할 수 있습니다.", + "description_raw": { + "ko": "언어팩을 활성화/비활성화/제거할 수 있습니다.", + "en": "Can activate/deactivate/uninstall language packs." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 198, + "identifier": "core.language_packs.update", + "name": "언어팩 업데이트", + "name_raw": { + "ko": "언어팩 업데이트", + "en": "Update Language Packs" + }, + "description": "설치된 언어팩의 업데이트를 확인하고 실행할 수 있습니다.", + "description_raw": { + "ko": "설치된 언어팩의 업데이트를 확인하고 실행할 수 있습니다.", + "en": "Can check and apply updates for installed language packs." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + } + ], + "leaf_count": 60 + }, + { + "id": 1, + "identifier": "apidoc-sample.parent", + "name": "API 문서 샘플 권한", + "name_raw": { + "ko": "API 문서 샘플 권한", + "en": "API Doc Sample Permission" + }, + "description": "문서 실측용 권한", + "description_raw": { + "ko": "문서 실측용 권한", + "en": "Sample permission" + }, + "extension_type": null, + "extension_identifier": null, + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 2, + "identifier": "apidoc-sample.child", + "name": "하위 권한", + "name_raw": { + "ko": "하위 권한", + "en": "Child Permission" + }, + "description": "Quod consequatur sunt necessitatibus quam quia voluptas quod aperiam.", + "description_raw": { + "ko": "Quod consequatur sunt necessitatibus quam quia voluptas quod aperiam.", + "en": "Mollitia incidunt dolor rem maxime." + }, + "extension_type": null, + "extension_identifier": null, + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 1 + }, + { + "id": 3, + "identifier": "sirsoft-board", + "name": "게시판", + "name_raw": { + "ko": "게시판", + "en": "Board" + }, + "description": "게시판 모듈 권한", + "description_raw": { + "ko": "게시판 모듈 권한", + "en": "Board module permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 4, + "identifier": "sirsoft-board.apidoc-sample-board", + "name": "API 문서 샘플 게시판 게시판", + "name_raw": { + "ko": "API 문서 샘플 게시판 게시판", + "en": "API Doc Sample Board board" + }, + "description": "API 문서 샘플 게시판 게시판 권한", + "description_raw": { + "ko": "API 문서 샘플 게시판 게시판 권한", + "en": "API Doc Sample Board board permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 5, + "identifier": "sirsoft-board.apidoc-sample-board.admin.posts.read", + "name": "게시글 조회 (관리자)", + "name_raw": { + "ko": "게시글 조회 (관리자)", + "en": "View Posts (Admin)" + }, + "description": "관리자 페이지에서 게시글 조회", + "description_raw": { + "ko": "관리자 페이지에서 게시글 조회", + "en": "View posts in admin page" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": "post", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 6, + "identifier": "sirsoft-board.apidoc-sample-board.admin.posts.write", + "name": "게시글 작성/수정/삭제 (관리자)", + "name_raw": { + "ko": "게시글 작성/수정/삭제 (관리자)", + "en": "Create/Edit/Delete Posts (Admin)" + }, + "description": "관리자 페이지에서 게시글 작성 및 수정/삭제", + "description_raw": { + "ko": "관리자 페이지에서 게시글 작성 및 수정/삭제", + "en": "Create and edit/delete posts in admin page" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 7, + "identifier": "sirsoft-board.apidoc-sample-board.admin.posts.read-secret", + "name": "비밀글 조회 (관리자)", + "name_raw": { + "ko": "비밀글 조회 (관리자)", + "en": "View Secret Posts (Admin)" + }, + "description": "관리자 페이지에서 비밀글 조회", + "description_raw": { + "ko": "관리자 페이지에서 비밀글 조회", + "en": "View secret posts in admin page" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": "post", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 8, + "identifier": "sirsoft-board.apidoc-sample-board.admin.comments.read", + "name": "댓글 조회 (관리자)", + "name_raw": { + "ko": "댓글 조회 (관리자)", + "en": "View Comments (Admin)" + }, + "description": "관리자 페이지에서 댓글 조회", + "description_raw": { + "ko": "관리자 페이지에서 댓글 조회", + "en": "View comments in admin page" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": "comment", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 9, + "identifier": "sirsoft-board.apidoc-sample-board.admin.comments.write", + "name": "댓글 작성/수정/삭제 (관리자)", + "name_raw": { + "ko": "댓글 작성/수정/삭제 (관리자)", + "en": "Create/Edit/Delete Comments (Admin)" + }, + "description": "관리자 페이지에서 댓글 작성 및 수정/삭제", + "description_raw": { + "ko": "관리자 페이지에서 댓글 작성 및 수정/삭제", + "en": "Create and edit/delete comments in admin page" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 10, + "identifier": "sirsoft-board.apidoc-sample-board.admin.attachments.upload", + "name": "파일 업로드/삭제 (관리자)", + "name_raw": { + "ko": "파일 업로드/삭제 (관리자)", + "en": "Upload/Delete Files (Admin)" + }, + "description": "관리자 페이지에서 파일 업로드 및 삭제", + "description_raw": { + "ko": "관리자 페이지에서 파일 업로드 및 삭제", + "en": "Upload and delete files in admin page" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": "attachment", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 11, + "identifier": "sirsoft-board.apidoc-sample-board.admin.attachments.download", + "name": "파일 다운로드 (관리자)", + "name_raw": { + "ko": "파일 다운로드 (관리자)", + "en": "Download Files (Admin)" + }, + "description": "관리자 페이지에서 파일 다운로드", + "description_raw": { + "ko": "관리자 페이지에서 파일 다운로드", + "en": "Download files in admin page" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": "attachment", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 12, + "identifier": "sirsoft-board.apidoc-sample-board.admin.manage", + "name": "게시판 관리 (관리자)", + "name_raw": { + "ko": "게시판 관리 (관리자)", + "en": "Manage Board (Admin)" + }, + "description": "타인 게시글/댓글 관리, 블라인드, 복원, 공지 설정", + "description_raw": { + "ko": "타인 게시글/댓글 관리, 블라인드, 복원, 공지 설정", + "en": "Manage others posts/comments, blind, restore, set notice" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": "board", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 8 + }, + { + "id": 95, + "identifier": "sirsoft-board.boards", + "name": "게시판 관리", + "name_raw": { + "ko": "게시판 관리", + "en": "Board Management" + }, + "description": "게시판 관리 권한", + "description_raw": { + "ko": "게시판 관리 권한", + "en": "Board management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 96, + "identifier": "sirsoft-board.boards.read", + "name": "게시판 조회", + "name_raw": { + "ko": "게시판 조회", + "en": "View Boards" + }, + "description": "게시판 목록 및 상세 조회", + "description_raw": { + "ko": "게시판 목록 및 상세 조회", + "en": "View board list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": "board", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 97, + "identifier": "sirsoft-board.boards.create", + "name": "게시판 생성", + "name_raw": { + "ko": "게시판 생성", + "en": "Create Board" + }, + "description": "새 게시판 생성", + "description_raw": { + "ko": "새 게시판 생성", + "en": "Create new board" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 98, + "identifier": "sirsoft-board.boards.update", + "name": "게시판 수정", + "name_raw": { + "ko": "게시판 수정", + "en": "Update Board" + }, + "description": "게시판 설정 수정", + "description_raw": { + "ko": "게시판 설정 수정", + "en": "Update board settings" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": "board", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 99, + "identifier": "sirsoft-board.boards.delete", + "name": "게시판 삭제", + "name_raw": { + "ko": "게시판 삭제", + "en": "Delete Board" + }, + "description": "게시판 삭제", + "description_raw": { + "ko": "게시판 삭제", + "en": "Delete board" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": "board", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + }, + { + "id": 100, + "identifier": "sirsoft-board.settings", + "name": "환경설정", + "name_raw": { + "ko": "환경설정", + "en": "Settings" + }, + "description": "게시판 환경설정 권한", + "description_raw": { + "ko": "게시판 환경설정 권한", + "en": "Board settings permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 101, + "identifier": "sirsoft-board.settings.read", + "name": "환경설정 조회", + "name_raw": { + "ko": "환경설정 조회", + "en": "View Settings" + }, + "description": "게시판 환경설정 조회", + "description_raw": { + "ko": "게시판 환경설정 조회", + "en": "View board settings" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 102, + "identifier": "sirsoft-board.settings.update", + "name": "환경설정 수정", + "name_raw": { + "ko": "환경설정 수정", + "en": "Update Settings" + }, + "description": "게시판 환경설정 수정", + "description_raw": { + "ko": "게시판 환경설정 수정", + "en": "Update board settings" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 2 + }, + { + "id": 103, + "identifier": "sirsoft-board.identity.policies", + "name": "게시판 본인인증 정책", + "name_raw": { + "ko": "게시판 본인인증 정책", + "en": "Board Identity Policies" + }, + "description": "게시판 컨텍스트의 본인인증 정책 관리 권한", + "description_raw": { + "ko": "게시판 컨텍스트의 본인인증 정책 관리 권한", + "en": "Manage identity verification policies in board context" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 104, + "identifier": "sirsoft-board.identity.policies.read", + "name": "본인인증 정책 조회", + "name_raw": { + "ko": "본인인증 정책 조회", + "en": "View Identity Policies" + }, + "description": "게시판 본인인증 정책 조회", + "description_raw": { + "ko": "게시판 본인인증 정책 조회", + "en": "View board identity policies" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 105, + "identifier": "sirsoft-board.identity.policies.update", + "name": "본인인증 정책 수정", + "name_raw": { + "ko": "본인인증 정책 수정", + "en": "Update Identity Policies" + }, + "description": "게시판 본인인증 정책 수정/추가/삭제", + "description_raw": { + "ko": "게시판 본인인증 정책 수정/추가/삭제", + "en": "Update, add, delete board identity policies" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 2 + }, + { + "id": 106, + "identifier": "sirsoft-board.reports", + "name": "게시판 신고 관리", + "name_raw": { + "ko": "게시판 신고 관리", + "en": "Report Management" + }, + "description": "게시판 신고 관리 권한", + "description_raw": { + "ko": "게시판 신고 관리 권한", + "en": "Board report management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 107, + "identifier": "sirsoft-board.reports.view", + "name": "신고 조회", + "name_raw": { + "ko": "신고 조회", + "en": "View Reports" + }, + "description": "신고 목록 및 상세 조회", + "description_raw": { + "ko": "신고 목록 및 상세 조회", + "en": "View report list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": "report", + "owner_key": "reporter_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 108, + "identifier": "sirsoft-board.reports.manage", + "name": "신고 처리", + "name_raw": { + "ko": "신고 처리", + "en": "Manage Reports" + }, + "description": "신고 상태 변경 및 처리", + "description_raw": { + "ko": "신고 상태 변경 및 처리", + "en": "Update report status and process" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": true, + "resource_route_key": "report", + "owner_key": "reporter_id", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 2 + } + ], + "leaf_count": 18 + }, + { + "id": 21, + "identifier": "sirsoft-ecommerce", + "name": "이커머스", + "name_raw": { + "ko": "이커머스", + "en": "Ecommerce" + }, + "description": "이커머스 모듈 권한", + "description_raw": { + "ko": "이커머스 모듈 권한", + "en": "Ecommerce module permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 22, + "identifier": "sirsoft-ecommerce.products", + "name": "상품 관리", + "name_raw": { + "ko": "상품 관리", + "en": "Product Management" + }, + "description": "상품 관리 권한", + "description_raw": { + "ko": "상품 관리 권한", + "en": "Product management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 23, + "identifier": "sirsoft-ecommerce.products.read", + "name": "상품 조회", + "name_raw": { + "ko": "상품 조회", + "en": "Read Products" + }, + "description": "상품 목록 및 상세 조회", + "description_raw": { + "ko": "상품 목록 및 상세 조회", + "en": "Read product list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "product", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 24, + "identifier": "sirsoft-ecommerce.products.create", + "name": "상품 생성", + "name_raw": { + "ko": "상품 생성", + "en": "Create Product" + }, + "description": "새 상품 등록", + "description_raw": { + "ko": "새 상품 등록", + "en": "Create new product" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 25, + "identifier": "sirsoft-ecommerce.products.update", + "name": "상품 수정", + "name_raw": { + "ko": "상품 수정", + "en": "Update Product" + }, + "description": "상품 정보 수정", + "description_raw": { + "ko": "상품 정보 수정", + "en": "Update product information" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "product", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 26, + "identifier": "sirsoft-ecommerce.products.delete", + "name": "상품 삭제", + "name_raw": { + "ko": "상품 삭제", + "en": "Delete Product" + }, + "description": "상품 삭제", + "description_raw": { + "ko": "상품 삭제", + "en": "Delete product" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "product", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + }, + { + "id": 27, + "identifier": "sirsoft-ecommerce.orders", + "name": "주문 관리", + "name_raw": { + "ko": "주문 관리", + "en": "Order Management" + }, + "description": "주문 관리 권한", + "description_raw": { + "ko": "주문 관리 권한", + "en": "Order management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 28, + "identifier": "sirsoft-ecommerce.orders.read", + "name": "주문 조회", + "name_raw": { + "ko": "주문 조회", + "en": "Read Orders" + }, + "description": "주문 목록 및 상세 조회", + "description_raw": { + "ko": "주문 목록 및 상세 조회", + "en": "Read order list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "order", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 29, + "identifier": "sirsoft-ecommerce.orders.update", + "name": "주문 수정", + "name_raw": { + "ko": "주문 수정", + "en": "Update Order" + }, + "description": "주문 상태 변경", + "description_raw": { + "ko": "주문 상태 변경", + "en": "Update order status" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "order", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 2 + }, + { + "id": 30, + "identifier": "sirsoft-ecommerce.categories", + "name": "카테고리 관리", + "name_raw": { + "ko": "카테고리 관리", + "en": "Category Management" + }, + "description": "카테고리 관리 권한", + "description_raw": { + "ko": "카테고리 관리 권한", + "en": "Category management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 31, + "identifier": "sirsoft-ecommerce.categories.read", + "name": "카테고리 조회", + "name_raw": { + "ko": "카테고리 조회", + "en": "Read Categories" + }, + "description": "카테고리 목록 및 상세 조회", + "description_raw": { + "ko": "카테고리 목록 및 상세 조회", + "en": "Read category list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 32, + "identifier": "sirsoft-ecommerce.categories.create", + "name": "카테고리 생성", + "name_raw": { + "ko": "카테고리 생성", + "en": "Create Category" + }, + "description": "카테고리 생성", + "description_raw": { + "ko": "카테고리 생성", + "en": "Create category" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 33, + "identifier": "sirsoft-ecommerce.categories.update", + "name": "카테고리 수정", + "name_raw": { + "ko": "카테고리 수정", + "en": "Update Category" + }, + "description": "카테고리 수정", + "description_raw": { + "ko": "카테고리 수정", + "en": "Update category" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 34, + "identifier": "sirsoft-ecommerce.categories.delete", + "name": "카테고리 삭제", + "name_raw": { + "ko": "카테고리 삭제", + "en": "Delete Category" + }, + "description": "카테고리 삭제", + "description_raw": { + "ko": "카테고리 삭제", + "en": "Delete category" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + }, + { + "id": 35, + "identifier": "sirsoft-ecommerce.brands", + "name": "브랜드 관리", + "name_raw": { + "ko": "브랜드 관리", + "en": "Brand Management" + }, + "description": "브랜드 관리 권한", + "description_raw": { + "ko": "브랜드 관리 권한", + "en": "Brand management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 36, + "identifier": "sirsoft-ecommerce.brands.read", + "name": "브랜드 조회", + "name_raw": { + "ko": "브랜드 조회", + "en": "Read Brands" + }, + "description": "브랜드 목록 및 상세 조회", + "description_raw": { + "ko": "브랜드 목록 및 상세 조회", + "en": "Read brand list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "brand", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 37, + "identifier": "sirsoft-ecommerce.brands.create", + "name": "브랜드 생성", + "name_raw": { + "ko": "브랜드 생성", + "en": "Create Brand" + }, + "description": "브랜드 생성", + "description_raw": { + "ko": "브랜드 생성", + "en": "Create brand" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 38, + "identifier": "sirsoft-ecommerce.brands.update", + "name": "브랜드 수정", + "name_raw": { + "ko": "브랜드 수정", + "en": "Update Brand" + }, + "description": "브랜드 수정", + "description_raw": { + "ko": "브랜드 수정", + "en": "Update brand" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "brand", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 39, + "identifier": "sirsoft-ecommerce.brands.delete", + "name": "브랜드 삭제", + "name_raw": { + "ko": "브랜드 삭제", + "en": "Delete Brand" + }, + "description": "브랜드 삭제", + "description_raw": { + "ko": "브랜드 삭제", + "en": "Delete brand" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "brand", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + }, + { + "id": 40, + "identifier": "sirsoft-ecommerce.product-notice-templates", + "name": "상품정보제공고시 관리", + "name_raw": { + "ko": "상품정보제공고시 관리", + "en": "Product Notice Template Management" + }, + "description": "상품정보제공고시 템플릿 관리 권한", + "description_raw": { + "ko": "상품정보제공고시 템플릿 관리 권한", + "en": "Product notice template management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 41, + "identifier": "sirsoft-ecommerce.product-notice-templates.read", + "name": "조회", + "name_raw": { + "ko": "조회", + "en": "Read" + }, + "description": "상품정보제공고시 조회", + "description_raw": { + "ko": "상품정보제공고시 조회", + "en": "Read product notice templates" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 42, + "identifier": "sirsoft-ecommerce.product-notice-templates.create", + "name": "생성", + "name_raw": { + "ko": "생성", + "en": "Create" + }, + "description": "상품정보제공고시 생성", + "description_raw": { + "ko": "상품정보제공고시 생성", + "en": "Create product notice template" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 43, + "identifier": "sirsoft-ecommerce.product-notice-templates.update", + "name": "수정", + "name_raw": { + "ko": "수정", + "en": "Update" + }, + "description": "상품정보제공고시 수정", + "description_raw": { + "ko": "상품정보제공고시 수정", + "en": "Update product notice template" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 44, + "identifier": "sirsoft-ecommerce.product-notice-templates.delete", + "name": "삭제", + "name_raw": { + "ko": "삭제", + "en": "Delete" + }, + "description": "상품정보제공고시 삭제", + "description_raw": { + "ko": "상품정보제공고시 삭제", + "en": "Delete product notice template" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + }, + { + "id": 45, + "identifier": "sirsoft-ecommerce.product-common-infos", + "name": "공통정보 관리", + "name_raw": { + "ko": "공통정보 관리", + "en": "Product Common Info Management" + }, + "description": "상품 공통정보 관리 권한", + "description_raw": { + "ko": "상품 공통정보 관리 권한", + "en": "Product common info management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 46, + "identifier": "sirsoft-ecommerce.product-common-infos.read", + "name": "조회", + "name_raw": { + "ko": "조회", + "en": "Read" + }, + "description": "공통정보 조회", + "description_raw": { + "ko": "공통정보 조회", + "en": "Read product common info" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 47, + "identifier": "sirsoft-ecommerce.product-common-infos.create", + "name": "생성", + "name_raw": { + "ko": "생성", + "en": "Create" + }, + "description": "공통정보 생성", + "description_raw": { + "ko": "공통정보 생성", + "en": "Create product common info" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 48, + "identifier": "sirsoft-ecommerce.product-common-infos.update", + "name": "수정", + "name_raw": { + "ko": "수정", + "en": "Update" + }, + "description": "공통정보 수정", + "description_raw": { + "ko": "공통정보 수정", + "en": "Update product common info" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 49, + "identifier": "sirsoft-ecommerce.product-common-infos.delete", + "name": "삭제", + "name_raw": { + "ko": "삭제", + "en": "Delete" + }, + "description": "공통정보 삭제", + "description_raw": { + "ko": "공통정보 삭제", + "en": "Delete product common info" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + }, + { + "id": 50, + "identifier": "sirsoft-ecommerce.settings", + "name": "환경설정", + "name_raw": { + "ko": "환경설정", + "en": "Settings" + }, + "description": "이커머스 환경설정 권한", + "description_raw": { + "ko": "이커머스 환경설정 권한", + "en": "Ecommerce settings permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 51, + "identifier": "sirsoft-ecommerce.settings.read", + "name": "환경설정 조회", + "name_raw": { + "ko": "환경설정 조회", + "en": "View Settings" + }, + "description": "이커머스 환경설정 조회", + "description_raw": { + "ko": "이커머스 환경설정 조회", + "en": "View ecommerce settings" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 52, + "identifier": "sirsoft-ecommerce.settings.update", + "name": "환경설정 수정", + "name_raw": { + "ko": "환경설정 수정", + "en": "Update Settings" + }, + "description": "이커머스 환경설정 수정", + "description_raw": { + "ko": "이커머스 환경설정 수정", + "en": "Update ecommerce settings" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 2 + }, + { + "id": 53, + "identifier": "sirsoft-ecommerce.promotion-coupon", + "name": "쿠폰 관리", + "name_raw": { + "ko": "쿠폰 관리", + "en": "Coupon Management" + }, + "description": "쿠폰 관리 권한", + "description_raw": { + "ko": "쿠폰 관리 권한", + "en": "Coupon management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 54, + "identifier": "sirsoft-ecommerce.promotion-coupon.read", + "name": "쿠폰 조회", + "name_raw": { + "ko": "쿠폰 조회", + "en": "Read Coupons" + }, + "description": "쿠폰 목록 및 상세 조회", + "description_raw": { + "ko": "쿠폰 목록 및 상세 조회", + "en": "Read coupon list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "coupon", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 55, + "identifier": "sirsoft-ecommerce.promotion-coupon.create", + "name": "쿠폰 생성", + "name_raw": { + "ko": "쿠폰 생성", + "en": "Create Coupon" + }, + "description": "새 쿠폰 등록", + "description_raw": { + "ko": "새 쿠폰 등록", + "en": "Create new coupon" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 56, + "identifier": "sirsoft-ecommerce.promotion-coupon.update", + "name": "쿠폰 수정", + "name_raw": { + "ko": "쿠폰 수정", + "en": "Update Coupon" + }, + "description": "쿠폰 정보 수정", + "description_raw": { + "ko": "쿠폰 정보 수정", + "en": "Update coupon information" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "coupon", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 57, + "identifier": "sirsoft-ecommerce.promotion-coupon.delete", + "name": "쿠폰 삭제", + "name_raw": { + "ko": "쿠폰 삭제", + "en": "Delete Coupon" + }, + "description": "쿠폰 삭제", + "description_raw": { + "ko": "쿠폰 삭제", + "en": "Delete coupon" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "coupon", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + }, + { + "id": 58, + "identifier": "sirsoft-ecommerce.shipping-policies", + "name": "배송정책 관리", + "name_raw": { + "ko": "배송정책 관리", + "en": "Shipping Policy Management" + }, + "description": "배송정책 관리 권한", + "description_raw": { + "ko": "배송정책 관리 권한", + "en": "Shipping policy management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 59, + "identifier": "sirsoft-ecommerce.shipping-policies.read", + "name": "배송정책 조회", + "name_raw": { + "ko": "배송정책 조회", + "en": "Read Shipping Policies" + }, + "description": "배송정책 목록 및 상세 조회", + "description_raw": { + "ko": "배송정책 목록 및 상세 조회", + "en": "Read shipping policy list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "shippingPolicy", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 60, + "identifier": "sirsoft-ecommerce.shipping-policies.create", + "name": "배송정책 생성", + "name_raw": { + "ko": "배송정책 생성", + "en": "Create Shipping Policy" + }, + "description": "새 배송정책 등록", + "description_raw": { + "ko": "새 배송정책 등록", + "en": "Create new shipping policy" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 61, + "identifier": "sirsoft-ecommerce.shipping-policies.update", + "name": "배송정책 수정", + "name_raw": { + "ko": "배송정책 수정", + "en": "Update Shipping Policy" + }, + "description": "배송정책 정보 수정", + "description_raw": { + "ko": "배송정책 정보 수정", + "en": "Update shipping policy information" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "shippingPolicy", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 62, + "identifier": "sirsoft-ecommerce.shipping-policies.delete", + "name": "배송정책 삭제", + "name_raw": { + "ko": "배송정책 삭제", + "en": "Delete Shipping Policy" + }, + "description": "배송정책 삭제", + "description_raw": { + "ko": "배송정책 삭제", + "en": "Delete shipping policy" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "shippingPolicy", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + }, + { + "id": 63, + "identifier": "sirsoft-ecommerce.product-labels", + "name": "상품 라벨 관리", + "name_raw": { + "ko": "상품 라벨 관리", + "en": "Product Label Management" + }, + "description": "상품 라벨 관리 권한", + "description_raw": { + "ko": "상품 라벨 관리 권한", + "en": "Product label management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 64, + "identifier": "sirsoft-ecommerce.product-labels.read", + "name": "상품 라벨 조회", + "name_raw": { + "ko": "상품 라벨 조회", + "en": "Read Product Labels" + }, + "description": "상품 라벨 목록 및 상세 조회", + "description_raw": { + "ko": "상품 라벨 목록 및 상세 조회", + "en": "Read product label list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 65, + "identifier": "sirsoft-ecommerce.product-labels.create", + "name": "상품 라벨 생성", + "name_raw": { + "ko": "상품 라벨 생성", + "en": "Create Product Label" + }, + "description": "새 상품 라벨 등록", + "description_raw": { + "ko": "새 상품 라벨 등록", + "en": "Create new product label" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 66, + "identifier": "sirsoft-ecommerce.product-labels.update", + "name": "상품 라벨 수정", + "name_raw": { + "ko": "상품 라벨 수정", + "en": "Update Product Label" + }, + "description": "상품 라벨 정보 수정", + "description_raw": { + "ko": "상품 라벨 정보 수정", + "en": "Update product label information" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 67, + "identifier": "sirsoft-ecommerce.product-labels.delete", + "name": "상품 라벨 삭제", + "name_raw": { + "ko": "상품 라벨 삭제", + "en": "Delete Product Label" + }, + "description": "상품 라벨 삭제", + "description_raw": { + "ko": "상품 라벨 삭제", + "en": "Delete product label" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + }, + { + "id": 68, + "identifier": "sirsoft-ecommerce.identity.policies", + "name": "이커머스 본인인증 정책", + "name_raw": { + "ko": "이커머스 본인인증 정책", + "en": "Ecommerce Identity Policies" + }, + "description": "이커머스 컨텍스트의 본인인증 정책 관리 권한", + "description_raw": { + "ko": "이커머스 컨텍스트의 본인인증 정책 관리 권한", + "en": "Manage identity verification policies in ecommerce context" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 69, + "identifier": "sirsoft-ecommerce.identity.policies.read", + "name": "본인인증 정책 조회", + "name_raw": { + "ko": "본인인증 정책 조회", + "en": "View Identity Policies" + }, + "description": "이커머스 본인인증 정책 조회", + "description_raw": { + "ko": "이커머스 본인인증 정책 조회", + "en": "View ecommerce identity policies" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 70, + "identifier": "sirsoft-ecommerce.identity.policies.update", + "name": "본인인증 정책 수정", + "name_raw": { + "ko": "본인인증 정책 수정", + "en": "Update Identity Policies" + }, + "description": "이커머스 본인인증 정책 수정/추가/삭제", + "description_raw": { + "ko": "이커머스 본인인증 정책 수정/추가/삭제", + "en": "Update, add, delete ecommerce identity policies" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 2 + }, + { + "id": 71, + "identifier": "sirsoft-ecommerce.reviews", + "name": "리뷰 관리", + "name_raw": { + "ko": "리뷰 관리", + "en": "Review Management" + }, + "description": "상품 리뷰 관리 권한", + "description_raw": { + "ko": "상품 리뷰 관리 권한", + "en": "Product review management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 72, + "identifier": "sirsoft-ecommerce.reviews.read", + "name": "리뷰 조회", + "name_raw": { + "ko": "리뷰 조회", + "en": "Read Reviews" + }, + "description": "리뷰 목록 및 상세 조회", + "description_raw": { + "ko": "리뷰 목록 및 상세 조회", + "en": "Read review list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "review", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 73, + "identifier": "sirsoft-ecommerce.reviews.update", + "name": "리뷰 처리", + "name_raw": { + "ko": "리뷰 처리", + "en": "Manage Review" + }, + "description": "리뷰 상태 변경, 답변 등록/수정/삭제, 일괄 처리", + "description_raw": { + "ko": "리뷰 상태 변경, 답변 등록/수정/삭제, 일괄 처리", + "en": "Update review status, manage replies, bulk actions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "review", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 74, + "identifier": "sirsoft-ecommerce.reviews.delete", + "name": "리뷰 삭제", + "name_raw": { + "ko": "리뷰 삭제", + "en": "Delete Review" + }, + "description": "리뷰 삭제", + "description_raw": { + "ko": "리뷰 삭제", + "en": "Delete review" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "review", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 3 + }, + { + "id": 75, + "identifier": "sirsoft-ecommerce.inquiries", + "name": "문의 관리", + "name_raw": { + "ko": "문의 관리", + "en": "Inquiry Management" + }, + "description": "상품 1:1 문의 관리 권한", + "description_raw": { + "ko": "상품 1:1 문의 관리 권한", + "en": "Product inquiry management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 76, + "identifier": "sirsoft-ecommerce.inquiries.update", + "name": "문의 처리", + "name_raw": { + "ko": "문의 처리", + "en": "Manage Inquiry" + }, + "description": "답변 등록/수정/삭제, 비밀글 내용 열람", + "description_raw": { + "ko": "답변 등록/수정/삭제, 비밀글 내용 열람", + "en": "Create, update, and delete inquiry replies; view secret inquiry contents" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 77, + "identifier": "sirsoft-ecommerce.inquiries.delete", + "name": "문의 삭제", + "name_raw": { + "ko": "문의 삭제", + "en": "Delete Inquiry" + }, + "description": "고객이 작성한 문의 자체를 삭제", + "description_raw": { + "ko": "고객이 작성한 문의 자체를 삭제", + "en": "Delete inquiries submitted by customers" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 2 + }, + { + "id": 78, + "identifier": "sirsoft-ecommerce.dashboard", + "name": "대시보드", + "name_raw": { + "ko": "대시보드", + "en": "Dashboard" + }, + "description": "관리자 대시보드 이커머스 영역 조회 권한", + "description_raw": { + "ko": "관리자 대시보드 이커머스 영역 조회 권한", + "en": "Admin dashboard commerce area view permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 79, + "identifier": "sirsoft-ecommerce.dashboard.view", + "name": "대시보드 조회", + "name_raw": { + "ko": "대시보드 조회", + "en": "View Dashboard" + }, + "description": "관리자 대시보드의 이커머스 판매 현황/리뷰/문의 위젯 조회", + "description_raw": { + "ko": "관리자 대시보드의 이커머스 판매 현황/리뷰/문의 위젯 조회", + "en": "View commerce sales/review/inquiry widgets on the admin dashboard" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 1 + }, + { + "id": 88, + "identifier": "sirsoft-ecommerce.mileage", + "name": "마일리지 관리", + "name_raw": { + "ko": "마일리지 관리", + "en": "Mileage Management" + }, + "description": "마일리지 내역 조회 및 수동 지급/차감 권한", + "description_raw": { + "ko": "마일리지 내역 조회 및 수동 지급/차감 권한", + "en": "Mileage history and manual grant/deduct permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 89, + "identifier": "sirsoft-ecommerce.mileage.read", + "name": "마일리지 내역 조회", + "name_raw": { + "ko": "마일리지 내역 조회", + "en": "Read Mileage" + }, + "description": "마일리지 내역 목록 및 상세 조회", + "description_raw": { + "ko": "마일리지 내역 목록 및 상세 조회", + "en": "Read mileage history list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "mileage-transaction", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 90, + "identifier": "sirsoft-ecommerce.mileage.manage", + "name": "마일리지 수동 처리", + "name_raw": { + "ko": "마일리지 수동 처리", + "en": "Manage Mileage" + }, + "description": "마일리지 수동 지급/차감 및 일괄 유효기간 연장", + "description_raw": { + "ko": "마일리지 수동 지급/차감 및 일괄 유효기간 연장", + "en": "Manual grant/deduct and bulk expiry extension" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": "mileage-transaction", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 2 + }, + { + "id": 91, + "identifier": "sirsoft-ecommerce.user-currency", + "name": "회원 결제 통화 관리", + "name_raw": { + "ko": "회원 결제 통화 관리", + "en": "User Currency Management" + }, + "description": "관리자가 회원별 결제 통화를 변경하는 권한", + "description_raw": { + "ko": "관리자가 회원별 결제 통화를 변경하는 권한", + "en": "Permission for admins to change a user's payment currency" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 92, + "identifier": "sirsoft-ecommerce.user-currency.manage", + "name": "회원 결제 통화 변경", + "name_raw": { + "ko": "회원 결제 통화 변경", + "en": "Manage User Currency" + }, + "description": "회원별 결제 통화 변경", + "description_raw": { + "ko": "회원별 결제 통화 변경", + "en": "Change a user's payment currency" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 1 + }, + { + "id": 93, + "identifier": "sirsoft-ecommerce.user-shipping-country", + "name": "회원 배송국가 관리", + "name_raw": { + "ko": "회원 배송국가 관리", + "en": "User Shipping Country Management" + }, + "description": "관리자가 회원별 배송국가를 변경하는 권한", + "description_raw": { + "ko": "관리자가 회원별 배송국가를 변경하는 권한", + "en": "Permission for admins to change a user's shipping country" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 94, + "identifier": "sirsoft-ecommerce.user-shipping-country.manage", + "name": "회원 배송국가 변경", + "name_raw": { + "ko": "회원 배송국가 변경", + "en": "Manage User Shipping Country" + }, + "description": "회원별 배송국가 변경", + "description_raw": { + "ko": "회원별 배송국가 변경", + "en": "Change a user's shipping country" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 1 + } + ], + "leaf_count": 48 + }, + { + "id": 109, + "identifier": "sirsoft-page", + "name": "페이지", + "name_raw": { + "ko": "페이지", + "en": "Page" + }, + "description": "페이지 모듈 권한", + "description_raw": { + "ko": "페이지 모듈 권한", + "en": "Page module permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 110, + "identifier": "sirsoft-page.pages", + "name": "페이지 관리", + "name_raw": { + "ko": "페이지 관리", + "en": "Page Management" + }, + "description": "페이지 관리 권한", + "description_raw": { + "ko": "페이지 관리 권한", + "en": "Page management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 111, + "identifier": "sirsoft-page.pages.read", + "name": "페이지 조회", + "name_raw": { + "ko": "페이지 조회", + "en": "View Pages" + }, + "description": "페이지 목록 및 상세 조회", + "description_raw": { + "ko": "페이지 목록 및 상세 조회", + "en": "View page list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "type": "admin", + "is_assignable": true, + "resource_route_key": "page", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 112, + "identifier": "sirsoft-page.pages.create", + "name": "페이지 생성", + "name_raw": { + "ko": "페이지 생성", + "en": "Create Page" + }, + "description": "새 페이지 생성", + "description_raw": { + "ko": "새 페이지 생성", + "en": "Create new page" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "type": "admin", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 113, + "identifier": "sirsoft-page.pages.update", + "name": "페이지 수정", + "name_raw": { + "ko": "페이지 수정", + "en": "Update Page" + }, + "description": "페이지 내용 수정", + "description_raw": { + "ko": "페이지 내용 수정", + "en": "Update page content" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "type": "admin", + "is_assignable": true, + "resource_route_key": "page", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 114, + "identifier": "sirsoft-page.pages.delete", + "name": "페이지 삭제", + "name_raw": { + "ko": "페이지 삭제", + "en": "Delete Page" + }, + "description": "페이지 삭제", + "description_raw": { + "ko": "페이지 삭제", + "en": "Delete page" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "type": "admin", + "is_assignable": true, + "resource_route_key": "page", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 4 + } + ], + "leaf_count": 4 + } + ] + }, + "user": { + "label": "사용자 권한", + "icon": "user", + "permissions": [ + { + "id": 115, + "identifier": "core", + "name": "코어", + "name_raw": { + "ko": "코어", + "en": "Core" + }, + "description": "코어 시스템 권한", + "description_raw": { + "ko": "코어 시스템 권한", + "en": "Core system permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 155, + "identifier": "core.user-notifications", + "name": "알림 (사용자)", + "name_raw": { + "ko": "알림 (사용자)", + "en": "Notifications (User)" + }, + "description": "사용자용 알림 권한 (사용자 화면에서 본인 알림 관리)", + "description_raw": { + "ko": "사용자용 알림 권한 (사용자 화면에서 본인 알림 관리)", + "en": "User notification permissions (managing own notifications in user UI)" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "user", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 156, + "identifier": "core.user-notifications.read", + "name": "알림 조회", + "name_raw": { + "ko": "알림 조회", + "en": "View Notifications" + }, + "description": "본인의 알림 목록 및 읽지 않은 수를 조회할 수 있습니다.", + "description_raw": { + "ko": "본인의 알림 목록 및 읽지 않은 수를 조회할 수 있습니다.", + "en": "Can view own notification list and unread count." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "user", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 157, + "identifier": "core.user-notifications.update", + "name": "알림 읽음 처리", + "name_raw": { + "ko": "알림 읽음 처리", + "en": "Mark Notifications Read" + }, + "description": "본인의 알림을 읽음 처리할 수 있습니다.", + "description_raw": { + "ko": "본인의 알림을 읽음 처리할 수 있습니다.", + "en": "Can mark own notifications as read." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "user", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 158, + "identifier": "core.user-notifications.delete", + "name": "알림 삭제", + "name_raw": { + "ko": "알림 삭제", + "en": "Delete Notifications" + }, + "description": "본인의 알림을 삭제할 수 있습니다.", + "description_raw": { + "ko": "본인의 알림을 삭제할 수 있습니다.", + "en": "Can delete own notifications." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "user", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 3 + }, + { + "id": 159, + "identifier": "core.identity", + "name": "본인인증 (사용자)", + "name_raw": { + "ko": "본인인증 (사용자)", + "en": "Identity Verification (User)" + }, + "description": "로그인 사용자의 본인인증 challenge 요청/검증/취소 권한", + "description_raw": { + "ko": "로그인 사용자의 본인인증 challenge 요청/검증/취소 권한", + "en": "Permissions for authenticated users to request/verify/cancel IDV challenges" + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "user", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 160, + "identifier": "core.identity.request", + "name": "IDV 요청", + "name_raw": { + "ko": "IDV 요청", + "en": "Request IDV Challenge" + }, + "description": "본인인증 challenge 를 요청할 수 있습니다.", + "description_raw": { + "ko": "본인인증 challenge 를 요청할 수 있습니다.", + "en": "Can request identity verification challenges." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "user", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 161, + "identifier": "core.identity.verify", + "name": "IDV 검증", + "name_raw": { + "ko": "IDV 검증", + "en": "Verify IDV Challenge" + }, + "description": "본인의 본인인증 challenge 를 검증할 수 있습니다.", + "description_raw": { + "ko": "본인의 본인인증 challenge 를 검증할 수 있습니다.", + "en": "Can verify own identity verification challenges." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "user", + "is_assignable": true, + "resource_route_key": "challenge", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 162, + "identifier": "core.identity.cancel", + "name": "IDV 취소", + "name_raw": { + "ko": "IDV 취소", + "en": "Cancel IDV Challenge" + }, + "description": "본인의 본인인증 challenge 를 취소할 수 있습니다.", + "description_raw": { + "ko": "본인의 본인인증 challenge 를 취소할 수 있습니다.", + "en": "Can cancel own identity verification challenges." + }, + "extension_type": "core", + "extension_identifier": "core", + "type": "user", + "is_assignable": true, + "resource_route_key": "challenge", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 3 + } + ], + "leaf_count": 6 + }, + { + "id": 3, + "identifier": "sirsoft-board", + "name": "게시판", + "name_raw": { + "ko": "게시판", + "en": "Board" + }, + "description": "게시판 모듈 권한", + "description_raw": { + "ko": "게시판 모듈 권한", + "en": "Board module permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 4, + "identifier": "sirsoft-board.apidoc-sample-board", + "name": "API 문서 샘플 게시판 게시판", + "name_raw": { + "ko": "API 문서 샘플 게시판 게시판", + "en": "API Doc Sample Board board" + }, + "description": "API 문서 샘플 게시판 게시판 권한", + "description_raw": { + "ko": "API 문서 샘플 게시판 게시판 권한", + "en": "API Doc Sample Board board permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 13, + "identifier": "sirsoft-board.apidoc-sample-board.posts.read", + "name": "게시글 조회", + "name_raw": { + "ko": "게시글 조회", + "en": "View Posts" + }, + "description": "게시글 목록 및 상세 조회", + "description_raw": { + "ko": "게시글 목록 및 상세 조회", + "en": "View post list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "user", + "is_assignable": true, + "resource_route_key": "post", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 14, + "identifier": "sirsoft-board.apidoc-sample-board.posts.write", + "name": "게시글 작성/수정/삭제", + "name_raw": { + "ko": "게시글 작성/수정/삭제", + "en": "Create/Edit/Delete Posts" + }, + "description": "게시글 작성 및 본인 글 수정/삭제", + "description_raw": { + "ko": "게시글 작성 및 본인 글 수정/삭제", + "en": "Create posts and edit/delete own posts" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "user", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 15, + "identifier": "sirsoft-board.apidoc-sample-board.posts.read-secret", + "name": "비밀글 조회", + "name_raw": { + "ko": "비밀글 조회", + "en": "View Secret Posts" + }, + "description": "비밀글 조회", + "description_raw": { + "ko": "비밀글 조회", + "en": "View secret posts" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "user", + "is_assignable": true, + "resource_route_key": "post", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 16, + "identifier": "sirsoft-board.apidoc-sample-board.comments.read", + "name": "댓글 조회", + "name_raw": { + "ko": "댓글 조회", + "en": "View Comments" + }, + "description": "댓글 목록 조회", + "description_raw": { + "ko": "댓글 목록 조회", + "en": "View comment list" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "user", + "is_assignable": true, + "resource_route_key": "comment", + "owner_key": "user_id", + "children": [], + "leaf_count": 1 + }, + { + "id": 17, + "identifier": "sirsoft-board.apidoc-sample-board.comments.write", + "name": "댓글 작성/수정/삭제", + "name_raw": { + "ko": "댓글 작성/수정/삭제", + "en": "Create/Edit/Delete Comments" + }, + "description": "댓글 작성 및 본인 댓글 수정/삭제", + "description_raw": { + "ko": "댓글 작성 및 본인 댓글 수정/삭제", + "en": "Create comments and edit/delete own comments" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "user", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 18, + "identifier": "sirsoft-board.apidoc-sample-board.attachments.upload", + "name": "파일 업로드/삭제", + "name_raw": { + "ko": "파일 업로드/삭제", + "en": "Upload/Delete Files" + }, + "description": "게시글에 파일 첨부 및 삭제", + "description_raw": { + "ko": "게시글에 파일 첨부 및 삭제", + "en": "Attach and delete files to posts" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "user", + "is_assignable": true, + "resource_route_key": "attachment", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 19, + "identifier": "sirsoft-board.apidoc-sample-board.attachments.download", + "name": "파일 다운로드", + "name_raw": { + "ko": "파일 다운로드", + "en": "Download Files" + }, + "description": "첨부파일 다운로드", + "description_raw": { + "ko": "첨부파일 다운로드", + "en": "Download attached files" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "user", + "is_assignable": true, + "resource_route_key": "attachment", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + }, + { + "id": 20, + "identifier": "sirsoft-board.apidoc-sample-board.manager", + "name": "게시판 관리 (사용자)", + "name_raw": { + "ko": "게시판 관리 (사용자)", + "en": "Manage Board (User)" + }, + "description": "사용자 페이지에서 게시판 관리", + "description_raw": { + "ko": "사용자 페이지에서 게시판 관리", + "en": "Manage board in user page" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "type": "user", + "is_assignable": true, + "resource_route_key": "board", + "owner_key": "created_by", + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 8 + } + ], + "leaf_count": 8 + }, + { + "id": 21, + "identifier": "sirsoft-ecommerce", + "name": "이커머스", + "name_raw": { + "ko": "이커머스", + "en": "Ecommerce" + }, + "description": "이커머스 모듈 권한", + "description_raw": { + "ko": "이커머스 모듈 권한", + "en": "Ecommerce module permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "admin", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 80, + "identifier": "sirsoft-ecommerce.user-products", + "name": "사용자 상품", + "name_raw": { + "ko": "사용자 상품", + "en": "User Products" + }, + "description": "사용자 상품 접근 권한 (블랙컨슈머 차단용)", + "description_raw": { + "ko": "사용자 상품 접근 권한 (블랙컨슈머 차단용)", + "en": "User product access permissions (for blocking malicious consumers)" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "user", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 81, + "identifier": "sirsoft-ecommerce.user-products.read", + "name": "상품 조회", + "name_raw": { + "ko": "상품 조회", + "en": "View Products" + }, + "description": "상품 목록 및 상세 조회", + "description_raw": { + "ko": "상품 목록 및 상세 조회", + "en": "View product list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "user", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 1 + }, + { + "id": 82, + "identifier": "sirsoft-ecommerce.user-orders", + "name": "사용자 주문", + "name_raw": { + "ko": "사용자 주문", + "en": "User Orders" + }, + "description": "사용자 주문 관련 권한 (블랙컨슈머 차단용)", + "description_raw": { + "ko": "사용자 주문 관련 권한 (블랙컨슈머 차단용)", + "en": "User order permissions (for blocking malicious consumers)" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "user", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 83, + "identifier": "sirsoft-ecommerce.user-orders.create", + "name": "주문하기", + "name_raw": { + "ko": "주문하기", + "en": "Create Order" + }, + "description": "주문 생성", + "description_raw": { + "ko": "주문 생성", + "en": "Create a new order" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "user", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 84, + "identifier": "sirsoft-ecommerce.user-orders.cancel", + "name": "주문 취소", + "name_raw": { + "ko": "주문 취소", + "en": "Cancel Order" + }, + "description": "주문 취소 요청", + "description_raw": { + "ko": "주문 취소 요청", + "en": "Request order cancellation" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "user", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + }, + { + "id": 85, + "identifier": "sirsoft-ecommerce.user-orders.confirm", + "name": "구매확정", + "name_raw": { + "ko": "구매확정", + "en": "Confirm Purchase" + }, + "description": "주문 상품 구매확정", + "description_raw": { + "ko": "주문 상품 구매확정", + "en": "Confirm purchase of order item" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "user", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 3 + }, + { + "id": 86, + "identifier": "sirsoft-ecommerce.user-reviews", + "name": "사용자 리뷰", + "name_raw": { + "ko": "사용자 리뷰", + "en": "User Reviews" + }, + "description": "사용자 리뷰 관련 권한 (블랙컨슈머 차단용)", + "description_raw": { + "ko": "사용자 리뷰 관련 권한 (블랙컨슈머 차단용)", + "en": "User review permissions (for blocking malicious consumers)" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "user", + "is_assignable": false, + "resource_route_key": null, + "owner_key": null, + "children": [ + { + "id": 87, + "identifier": "sirsoft-ecommerce.user-reviews.write", + "name": "리뷰 작성", + "name_raw": { + "ko": "리뷰 작성", + "en": "Write Review" + }, + "description": "상품 리뷰 작성", + "description_raw": { + "ko": "상품 리뷰 작성", + "en": "Write product review" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "type": "user", + "is_assignable": true, + "resource_route_key": null, + "owner_key": null, + "children": [], + "leaf_count": 1 + } + ], + "leaf_count": 1 + } + ], + "leaf_count": 5 + } + ] + } + }, + "types": [ + "admin", + "user" + ], + "0": "... (총 4건 중 2건 표시)" + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/plugins.md b/docs/backend/api/plugins.md index 71e414a8..745557a3 100644 --- a/docs/backend/api/plugins.md +++ b/docs/backend/api/plugins.md @@ -36,6 +36,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.index_validation_rules`). +**요청 예시** + +```http +GET /api/admin/plugins?search=%EC%98%88%EC%8B%9C%EA%B0%92&filters=%EC%98%88%EC%8B%9C%EA%B0%92&status=installed&per_page=1&page=1&include_hidden=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -71,6 +80,118 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | incompatible_required_version | null | `null` | 요구 코어 버전 미충족 시 필요한 버전 (호환되면 null) | | abilities | object | `{"can_install":true,"can_activate":true,"can_uninstall":t…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "플러그인 목록을 성공적으로 가져왔습니다.", + "data": { + "data": [ + { + "id": null, + "identifier": "sirsoft-ckeditor5", + "vendor": "sirsoft", + "name": "CKEditor 5 WYSIWYG 에디터", + "version": "1.0.0", + "description": "CKEditor 5를 이용한 WYSIWYG 에디터 플러그인입니다. 플러그인 설치만으로 기존 HtmlEditor가 교체됩니다.", + "dependencies": [], + "permissions": [], + "roles": [], + "config": [], + "hooks": [], + "status": "uninstalled", + "is_installed": false, + "has_settings": true, + "settings_route": "/admin/plugins/sirsoft-ckeditor5/settings", + "assets": { + "js": "/api/plugins/assets/sirsoft-ckeditor5/dist/js/plugin.iife.js", + "css": null, + "priority": 100 + }, + "update_available": false, + "update_source": null, + "latest_version": null, + "file_version": null, + "github_url": null, + "github_changelog_url": null, + "is_pending": false, + "is_bundled": false, + "deactivated_reason": null, + "deactivated_at": null, + "incompatible_required_version": null, + "abilities": { + "can_install": true, + "can_activate": true, + "can_uninstall": true + } + }, + { + "id": null, + "identifier": "sirsoft-daum_postcode", + "vendor": "sirsoft", + "name": "Daum 우편번호", + "version": "1.0.0", + "description": "Daum 우편번호 서비스를 통한 주소 검색 기능을 제공하는 플러그인입니다. API 키 없이 무료로 사용할 수 있습니다.", + "dependencies": [], + "permissions": [], + "roles": [], + "config": [], + "hooks": [], + "status": "uninstalled", + "is_installed": false, + "has_settings": true, + "settings_route": "/admin/plugins/sirsoft-daum_postcode/settings", + "assets": { + "js": "/api/plugins/assets/sirsoft-daum_postcode/dist/js/plugin.iife.js", + "css": null, + "priority": 100 + }, + "update_available": false, + "update_source": null, + "latest_version": null, + "file_version": null, + "github_url": null, + "github_changelog_url": null, + "is_pending": false, + "is_bundled": false, + "deactivated_reason": null, + "deactivated_at": null, + "incompatible_required_version": null, + "abilities": { + "can_install": true, + "can_activate": true, + "can_uninstall": true + } + }, + "... (총 9건 중 2건 표시)" + ], + "pagination": { + "total": 9, + "current_page": 1, + "last_page": 1, + "per_page": 25 + }, + "meta": { + "total_plugins": 9, + "active_plugins": 0, + "inactive_plugins": 0, + "installed_plugins": 0, + "uninstalled_plugins": 9 + }, + "abilities": { + "can_install": true, + "can_activate": true, + "can_uninstall": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -99,10 +220,29 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.activate_validation_rules`). +**요청 예시** + +```http +POST /api/admin/plugins/activate HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "plugin_name": "예시 이름", + "force": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -126,10 +266,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/plugins/check-updates HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -157,10 +310,29 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.deactivate_validation_rules`). +**요청 예시** + +```http +POST /api/admin/plugins/deactivate HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "plugin_name": "예시 이름", + "force": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -191,10 +363,35 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.install_validation_rules`). +**요청 예시** + +```http +POST /api/admin/plugins/install HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "plugin_name": "예시 이름", + "vendor_mode": "auto", + "dependencies": [ + "예시값" + ], + "language_packs": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -222,10 +419,31 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.install_from_file_validation_rules`). +**요청 예시** + +```http +POST /api/admin/plugins/install-from-file HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -253,10 +471,28 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.install_from_github_validation_rules`). +**요청 예시** + +```http +POST /api/admin/plugins/install-from-github HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "github_url": "https://example.com" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -280,6 +516,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/plugins/installed HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -315,6 +560,29 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | incompatible_required_version | null | `null` | 요구 코어 버전 미충족 시 필요한 버전 (호환되면 null) | | abilities | object | `{"can_install":true,"can_activate":true,"can_uninstall":t…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "플러그인 목록을 성공적으로 가져왔습니다.", + "data": { + "data": [], + "meta": { + "total_plugins": 0, + "active_plugins": 0, + "inactive_plugins": 0, + "installed_plugins": 0, + "uninstalled_plugins": 0 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -338,10 +606,31 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | file | body | file | 예 | max 51200 | 업로드 파일 | +**요청 예시** + +```http +POST /api/admin/plugins/manifest-preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -369,10 +658,28 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.refresh_layouts_validation_rules`). +**요청 예시** + +```http +POST /api/admin/plugins/refresh-layouts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "plugin_name": "예시 이름" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -401,10 +708,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.uninstall_validation_rules`). +**요청 예시** + +```http +DELETE /api/admin/plugins/uninstall?plugin_name=%EC%98%88%EC%8B%9C%20%EC%9D%B4%EB%A6%84&delete_data=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -435,10 +755,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.extension.changelog_rules`). +**요청 예시** + +```http +GET /api/admin/plugins/{identifier}/changelog?source=active&from_version=%EC%98%88%EC%8B%9C%EA%B0%92&to_version=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -465,10 +798,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/plugins/{identifier}/dependent-templates HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -494,10 +840,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/plugins/{identifier}/license HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -523,10 +882,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/plugins/{identifier}/settings HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -554,10 +926,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin_settings.update_rules`). +**요청 예시** + +```http +PUT /api/admin/plugins/{identifier}/settings HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -584,10 +969,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/plugins/{identifier}/settings/layout HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -613,10 +1011,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | pluginName | path | string | 예 | — | 대상 plugin의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/plugins/{pluginName} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -642,10 +1053,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | pluginName | path | string | 예 | — | 대상 plugin의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/plugins/{pluginName}/check-modified-layouts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -671,10 +1095,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | pluginName | path | string | 예 | — | 대상 plugin의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/plugins/{pluginName}/install-preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -700,10 +1137,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | pluginName | path | string | 예 | — | 대상 plugin의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/plugins/{pluginName}/uninstall-info HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -734,10 +1184,30 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.perform_update_validation_rules`). +**요청 예시** + +```http +POST /api/admin/plugins/{pluginName}/update HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "layout_strategy": "overwrite", + "vendor_mode": "auto", + "force": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -764,13 +1234,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | | path | path | string | 예 | — | 경로 | -| identifier | query | string | 예 | — | 대상 확장/리소스의 식별자 | -| path | query | string | 예 | — | 경로 | + +**요청 예시** + +```http +GET /api/plugins/assets/{identifier}/{path} HTTP/1.1 +Host: api.example.com +Accept: application/json +``` **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -793,10 +1273,22 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/bundle.css HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** _대표 에러 없음 (공개 조회 — 활성 에셋이 없으면 빈 200 응답)._ @@ -816,10 +1308,22 @@ _대표 에러 없음 (공개 조회 — 활성 에셋이 없으면 빈 200 응 _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/bundle.js HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** _대표 에러 없음 (공개 조회 — 활성 에셋이 없으면 빈 200 응답)._ @@ -841,10 +1345,22 @@ _대표 에러 없음 (공개 조회 — 활성 에셋이 없으면 빈 200 응 | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/plugins/{identifier}/components.json HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -868,10 +1384,22 @@ _대표 에러 없음 (공개 조회 — 활성 에셋이 없으면 빈 200 응 | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/plugins/{identifier}/editor-spec HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/profile.md b/docs/backend/api/profile.md index caea6ea1..90cbbda1 100644 --- a/docs/backend/api/profile.md +++ b/docs/backend/api/profile.md @@ -27,10 +27,23 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/user/profile HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -81,10 +94,47 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.update_profile_validation_rules`). +**요청 예시** + +```http +PUT /api/user/profile HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +Content-Type: application/json + +{ + "name": "예시 이름", + "nickname": "예시 이름", + "email": "user@example.com", + "password": "Password123!", + "current_password": "Password123!", + "language": "ko", + "country": "KR", + "timezone": "Asia/Seoul", + "homepage": "https://example.com", + "mobile": "010-1234-5678", + "phone": "010-1234-5678", + "zipcode": "06234", + "address": "서울특별시 강남구 테헤란로 1", + "address_detail": "서울특별시 강남구 테헤란로 1", + "signature": "예시값", + "bio": "예시 내용입니다.", + "notify_post_complete": true, + "notify_post_reply": true, + "notify_comment": true, + "notify_reply_comment": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -109,10 +159,23 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/user/profile/activity-log HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -136,10 +199,23 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/user/profile/update-language HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/roles.md b/docs/backend/api/roles.md index 420fee88..1c03b361 100644 --- a/docs/backend/api/roles.md +++ b/docs/backend/api/roles.md @@ -32,6 +32,15 @@ | search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | | is_active | query | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +**요청 예시** + +```http +GET /api/admin/roles?page=1&per_page=1&search=%EC%98%88%EC%8B%9C%EA%B0%92&is_active=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -57,6 +66,2574 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | updated_at | string | `2026-06-04 09:35:35` | 최종 수정 일시 | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "역할 정보를 성공적으로 가져왔습니다.", + "data": { + "data": [ + { + "id": 6, + "identifier": "manager", + "name": "매니저", + "name_raw": { + "ko": "매니저", + "en": "Manager" + }, + "description": "콘텐츠 및 사용자 관리 권한을 가진 관리자입니다.", + "description_raw": { + "ko": "콘텐츠 및 사용자 관리 권한을 가진 관리자입니다.", + "en": "Manager with content and user management permissions." + }, + "extension_type": "core", + "extension_identifier": "core", + "extension_name": null, + "is_deletable": false, + "is_active": true, + "users_count": 0, + "permission_ids": [ + 117, + 118, + 119, + 122, + 123, + 124, + 149, + 176, + 177, + 179, + 182, + 185, + 186, + 187 + ], + "permission_values": [ + { + "id": 117, + "scope_type": "self" + }, + { + "id": 118, + "scope_type": null + }, + { + "id": 119, + "scope_type": "self" + }, + { + "id": 122, + "scope_type": null + }, + { + "id": 123, + "scope_type": null + }, + { + "id": 124, + "scope_type": null + }, + { + "id": 149, + "scope_type": "self" + }, + { + "id": 176, + "scope_type": null + }, + { + "id": 177, + "scope_type": null + }, + { + "id": 179, + "scope_type": null + }, + { + "id": 182, + "scope_type": "self" + }, + { + "id": 185, + "scope_type": null + }, + { + "id": 186, + "scope_type": "self" + }, + { + "id": 187, + "scope_type": "self" + } + ], + "permissions": [ + { + "id": 115, + "parent_id": null, + "identifier": "core", + "name": "코어", + "name_raw": { + "ko": "코어", + "en": "Core" + }, + "description": "코어 시스템 권한", + "description_raw": { + "ko": "코어 시스템 권한", + "en": "Core system permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 116, + "parent_id": 115, + "identifier": "core.users", + "name": "사용자 관리", + "name_raw": { + "ko": "사용자 관리", + "en": "User Management" + }, + "description": "사용자 관리 권한", + "description_raw": { + "ko": "사용자 관리 권한", + "en": "User management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 117, + "parent_id": 116, + "identifier": "core.users.read", + "name": "사용자 조회", + "name_raw": { + "ko": "사용자 조회", + "en": "View Users" + }, + "description": "사용자 목록 및 상세 정보를 조회할 수 있습니다.", + "description_raw": { + "ko": "사용자 목록 및 상세 정보를 조회할 수 있습니다.", + "en": "Can view user list and details." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "user", + "owner_key": "id", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": "self" + }, + { + "id": 118, + "parent_id": 116, + "identifier": "core.users.create", + "name": "사용자 생성", + "name_raw": { + "ko": "사용자 생성", + "en": "Create Users" + }, + "description": "새로운 사용자를 생성할 수 있습니다.", + "description_raw": { + "ko": "새로운 사용자를 생성할 수 있습니다.", + "en": "Can create new users." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 119, + "parent_id": 116, + "identifier": "core.users.update", + "name": "사용자 수정", + "name_raw": { + "ko": "사용자 수정", + "en": "Update Users" + }, + "description": "사용자 정보를 수정할 수 있습니다.", + "description_raw": { + "ko": "사용자 정보를 수정할 수 있습니다.", + "en": "Can update user information." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "user", + "owner_key": "id", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": "self" + } + ] + }, + { + "id": 121, + "parent_id": 115, + "identifier": "core.menus", + "name": "메뉴 관리", + "name_raw": { + "ko": "메뉴 관리", + "en": "Menu Management" + }, + "description": "메뉴 관리 권한", + "description_raw": { + "ko": "메뉴 관리 권한", + "en": "Menu management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 122, + "parent_id": 121, + "identifier": "core.menus.read", + "name": "메뉴 조회", + "name_raw": { + "ko": "메뉴 조회", + "en": "View Menus" + }, + "description": "메뉴 목록을 조회할 수 있습니다.", + "description_raw": { + "ko": "메뉴 목록을 조회할 수 있습니다.", + "en": "Can view menu list." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "menu", + "owner_key": "created_by", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 123, + "parent_id": 121, + "identifier": "core.menus.create", + "name": "메뉴 생성", + "name_raw": { + "ko": "메뉴 생성", + "en": "Create Menus" + }, + "description": "새로운 메뉴를 생성할 수 있습니다.", + "description_raw": { + "ko": "새로운 메뉴를 생성할 수 있습니다.", + "en": "Can create new menus." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 124, + "parent_id": 121, + "identifier": "core.menus.update", + "name": "메뉴 수정", + "name_raw": { + "ko": "메뉴 수정", + "en": "Update Menus" + }, + "description": "메뉴 정보를 수정할 수 있습니다.", + "description_raw": { + "ko": "메뉴 정보를 수정할 수 있습니다.", + "en": "Can update menu information." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "menu", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 148, + "parent_id": 115, + "identifier": "core.notification-logs", + "name": "알림 발송 이력", + "name_raw": { + "ko": "알림 발송 이력", + "en": "Notification Logs" + }, + "description": "알림 발송 이력 관리 권한", + "description_raw": { + "ko": "알림 발송 이력 관리 권한", + "en": "Notification log management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 7, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 149, + "parent_id": 148, + "identifier": "core.notification-logs.read", + "name": "발송 이력 조회", + "name_raw": { + "ko": "발송 이력 조회", + "en": "View Notification Logs" + }, + "description": "알림 발송 이력을 조회할 수 있습니다.", + "description_raw": { + "ko": "알림 발송 이력을 조회할 수 있습니다.", + "en": "Can view notification logs." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": "self" + } + ] + }, + { + "id": 175, + "parent_id": 115, + "identifier": "core.dashboard", + "name": "대시보드", + "name_raw": { + "ko": "대시보드", + "en": "Dashboard" + }, + "description": "대시보드 접근 권한", + "description_raw": { + "ko": "대시보드 접근 권한", + "en": "Dashboard access permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 9, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 176, + "parent_id": 175, + "identifier": "core.dashboard.read", + "name": "대시보드 조회", + "name_raw": { + "ko": "대시보드 조회", + "en": "View Dashboard" + }, + "description": "대시보드 통계 및 정보를 조회할 수 있습니다.", + "description_raw": { + "ko": "대시보드 통계 및 정보를 조회할 수 있습니다.", + "en": "Can view dashboard statistics and information." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 177, + "parent_id": 175, + "identifier": "core.dashboard.system-status", + "name": "시스템 상태", + "name_raw": { + "ko": "시스템 상태", + "en": "System Status" + }, + "description": "시스템 상태 정보를 조회할 수 있습니다.", + "description_raw": { + "ko": "시스템 상태 정보를 조회할 수 있습니다.", + "en": "Can view system status information." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 179, + "parent_id": 175, + "identifier": "core.dashboard.activities", + "name": "최근 활동", + "name_raw": { + "ko": "최근 활동", + "en": "Recent Activities" + }, + "description": "최근 활동 이력을 조회할 수 있습니다.", + "description_raw": { + "ko": "최근 활동 이력을 조회할 수 있습니다.", + "en": "Can view recent activity history." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "activityLog", + "owner_key": "user_id", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 181, + "parent_id": 115, + "identifier": "core.activities", + "name": "활동 로그", + "name_raw": { + "ko": "활동 로그", + "en": "Activity Logs" + }, + "description": "활동 로그 조회 권한", + "description_raw": { + "ko": "활동 로그 조회 권한", + "en": "Activity log access permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 10, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 182, + "parent_id": 181, + "identifier": "core.activities.read", + "name": "활동 로그 조회", + "name_raw": { + "ko": "활동 로그 조회", + "en": "View Activity Logs" + }, + "description": "활동 로그를 조회할 수 있습니다.", + "description_raw": { + "ko": "활동 로그를 조회할 수 있습니다.", + "en": "Can view activity logs." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "activityLog", + "owner_key": "user_id", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": "self" + } + ] + }, + { + "id": 184, + "parent_id": 115, + "identifier": "core.attachments", + "name": "첨부파일 관리", + "name_raw": { + "ko": "첨부파일 관리", + "en": "Attachment Management" + }, + "description": "첨부파일 관리 권한", + "description_raw": { + "ko": "첨부파일 관리 권한", + "en": "Attachment management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 11, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 185, + "parent_id": 184, + "identifier": "core.attachments.create", + "name": "첨부파일 업로드", + "name_raw": { + "ko": "첨부파일 업로드", + "en": "Upload Attachments" + }, + "description": "첨부파일을 업로드할 수 있습니다.", + "description_raw": { + "ko": "첨부파일을 업로드할 수 있습니다.", + "en": "Can upload attachments." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 186, + "parent_id": 184, + "identifier": "core.attachments.update", + "name": "첨부파일 수정", + "name_raw": { + "ko": "첨부파일 수정", + "en": "Update Attachments" + }, + "description": "첨부파일 정보 및 순서를 수정할 수 있습니다.", + "description_raw": { + "ko": "첨부파일 정보 및 순서를 수정할 수 있습니다.", + "en": "Can update attachment information and order." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "attachment", + "owner_key": "created_by", + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": "self" + }, + { + "id": 187, + "parent_id": 184, + "identifier": "core.attachments.delete", + "name": "첨부파일 삭제", + "name_raw": { + "ko": "첨부파일 삭제", + "en": "Delete Attachments" + }, + "description": "첨부파일을 삭제할 수 있습니다.", + "description_raw": { + "ko": "첨부파일을 삭제할 수 있습니다.", + "en": "Can delete attachments." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "attachment", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": "self" + } + ] + } + ] + } + ], + "created_at": "2026-07-08 10:47:51", + "updated_at": "2026-07-08 10:47:51", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_toggle_status": true + } + }, + { + "id": 5, + "identifier": "sirsoft-ecommerce.manager", + "name": "이커머스 관리자", + "name_raw": { + "ko": "이커머스 관리자", + "en": "Ecommerce Manager" + }, + "description": "이커머스 관리 권한을 가진 역할", + "description_raw": { + "ko": "이커머스 관리 권한을 가진 역할", + "en": "Role with ecommerce management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "extension_name": "이커머스", + "is_deletable": false, + "is_active": true, + "users_count": 0, + "permission_ids": [ + 23, + 24, + 25, + 26, + 28, + 29, + 31, + 32, + 33, + 34, + 36, + 37, + 38, + 39, + 41, + 42, + 43, + 44, + 46, + 47, + 48, + 49, + 51, + 52, + 54, + 55, + 56, + 57, + 59, + 60, + 61, + 62, + 64, + 65, + 66, + 67, + 72, + 73, + 74, + 76, + 77, + 79, + 81, + 83, + 84, + 85, + 87, + 89, + 90, + 92, + 94 + ], + "permission_values": [ + { + "id": 23, + "scope_type": null + }, + { + "id": 24, + "scope_type": null + }, + { + "id": 25, + "scope_type": null + }, + { + "id": 26, + "scope_type": null + }, + { + "id": 28, + "scope_type": null + }, + { + "id": 29, + "scope_type": null + }, + { + "id": 31, + "scope_type": null + }, + { + "id": 32, + "scope_type": null + }, + { + "id": 33, + "scope_type": null + }, + { + "id": 34, + "scope_type": null + }, + { + "id": 36, + "scope_type": null + }, + { + "id": 37, + "scope_type": null + }, + { + "id": 38, + "scope_type": null + }, + { + "id": 39, + "scope_type": null + }, + { + "id": 41, + "scope_type": null + }, + { + "id": 42, + "scope_type": null + }, + { + "id": 43, + "scope_type": null + }, + { + "id": 44, + "scope_type": null + }, + { + "id": 46, + "scope_type": null + }, + { + "id": 47, + "scope_type": null + }, + { + "id": 48, + "scope_type": null + }, + { + "id": 49, + "scope_type": null + }, + { + "id": 51, + "scope_type": null + }, + { + "id": 52, + "scope_type": null + }, + { + "id": 54, + "scope_type": null + }, + { + "id": 55, + "scope_type": null + }, + { + "id": 56, + "scope_type": null + }, + { + "id": 57, + "scope_type": null + }, + { + "id": 59, + "scope_type": null + }, + { + "id": 60, + "scope_type": null + }, + { + "id": 61, + "scope_type": null + }, + { + "id": 62, + "scope_type": null + }, + { + "id": 64, + "scope_type": null + }, + { + "id": 65, + "scope_type": null + }, + { + "id": 66, + "scope_type": null + }, + { + "id": 67, + "scope_type": null + }, + { + "id": 72, + "scope_type": null + }, + { + "id": 73, + "scope_type": null + }, + { + "id": 74, + "scope_type": null + }, + { + "id": 76, + "scope_type": null + }, + { + "id": 77, + "scope_type": null + }, + { + "id": 79, + "scope_type": null + }, + { + "id": 81, + "scope_type": null + }, + { + "id": 83, + "scope_type": null + }, + { + "id": 84, + "scope_type": null + }, + { + "id": 85, + "scope_type": null + }, + { + "id": 87, + "scope_type": null + }, + { + "id": 89, + "scope_type": null + }, + { + "id": 90, + "scope_type": null + }, + { + "id": 92, + "scope_type": null + }, + { + "id": 94, + "scope_type": null + } + ], + "permissions": [ + { + "id": 21, + "parent_id": null, + "identifier": "sirsoft-ecommerce", + "name": "이커머스", + "name_raw": { + "ko": "이커머스", + "en": "Ecommerce" + }, + "description": "이커머스 모듈 권한", + "description_raw": { + "ko": "이커머스 모듈 권한", + "en": "Ecommerce module permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 100, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 22, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.products", + "name": "상품 관리", + "name_raw": { + "ko": "상품 관리", + "en": "Product Management" + }, + "description": "상품 관리 권한", + "description_raw": { + "ko": "상품 관리 권한", + "en": "Product management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 23, + "parent_id": 22, + "identifier": "sirsoft-ecommerce.products.read", + "name": "상품 조회", + "name_raw": { + "ko": "상품 조회", + "en": "Read Products" + }, + "description": "상품 목록 및 상세 조회", + "description_raw": { + "ko": "상품 목록 및 상세 조회", + "en": "Read product list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "product", + "owner_key": "created_by", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 24, + "parent_id": 22, + "identifier": "sirsoft-ecommerce.products.create", + "name": "상품 생성", + "name_raw": { + "ko": "상품 생성", + "en": "Create Product" + }, + "description": "새 상품 등록", + "description_raw": { + "ko": "새 상품 등록", + "en": "Create new product" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 25, + "parent_id": 22, + "identifier": "sirsoft-ecommerce.products.update", + "name": "상품 수정", + "name_raw": { + "ko": "상품 수정", + "en": "Update Product" + }, + "description": "상품 정보 수정", + "description_raw": { + "ko": "상품 정보 수정", + "en": "Update product information" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "product", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 26, + "parent_id": 22, + "identifier": "sirsoft-ecommerce.products.delete", + "name": "상품 삭제", + "name_raw": { + "ko": "상품 삭제", + "en": "Delete Product" + }, + "description": "상품 삭제", + "description_raw": { + "ko": "상품 삭제", + "en": "Delete product" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "product", + "owner_key": "created_by", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 27, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.orders", + "name": "주문 관리", + "name_raw": { + "ko": "주문 관리", + "en": "Order Management" + }, + "description": "주문 관리 권한", + "description_raw": { + "ko": "주문 관리 권한", + "en": "Order management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 28, + "parent_id": 27, + "identifier": "sirsoft-ecommerce.orders.read", + "name": "주문 조회", + "name_raw": { + "ko": "주문 조회", + "en": "Read Orders" + }, + "description": "주문 목록 및 상세 조회", + "description_raw": { + "ko": "주문 목록 및 상세 조회", + "en": "Read order list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "order", + "owner_key": "user_id", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 29, + "parent_id": 27, + "identifier": "sirsoft-ecommerce.orders.update", + "name": "주문 수정", + "name_raw": { + "ko": "주문 수정", + "en": "Update Order" + }, + "description": "주문 상태 변경", + "description_raw": { + "ko": "주문 상태 변경", + "en": "Update order status" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "order", + "owner_key": "user_id", + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 30, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.categories", + "name": "카테고리 관리", + "name_raw": { + "ko": "카테고리 관리", + "en": "Category Management" + }, + "description": "카테고리 관리 권한", + "description_raw": { + "ko": "카테고리 관리 권한", + "en": "Category management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 31, + "parent_id": 30, + "identifier": "sirsoft-ecommerce.categories.read", + "name": "카테고리 조회", + "name_raw": { + "ko": "카테고리 조회", + "en": "Read Categories" + }, + "description": "카테고리 목록 및 상세 조회", + "description_raw": { + "ko": "카테고리 목록 및 상세 조회", + "en": "Read category list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 32, + "parent_id": 30, + "identifier": "sirsoft-ecommerce.categories.create", + "name": "카테고리 생성", + "name_raw": { + "ko": "카테고리 생성", + "en": "Create Category" + }, + "description": "카테고리 생성", + "description_raw": { + "ko": "카테고리 생성", + "en": "Create category" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 33, + "parent_id": 30, + "identifier": "sirsoft-ecommerce.categories.update", + "name": "카테고리 수정", + "name_raw": { + "ko": "카테고리 수정", + "en": "Update Category" + }, + "description": "카테고리 수정", + "description_raw": { + "ko": "카테고리 수정", + "en": "Update category" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 34, + "parent_id": 30, + "identifier": "sirsoft-ecommerce.categories.delete", + "name": "카테고리 삭제", + "name_raw": { + "ko": "카테고리 삭제", + "en": "Delete Category" + }, + "description": "카테고리 삭제", + "description_raw": { + "ko": "카테고리 삭제", + "en": "Delete category" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 35, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.brands", + "name": "브랜드 관리", + "name_raw": { + "ko": "브랜드 관리", + "en": "Brand Management" + }, + "description": "브랜드 관리 권한", + "description_raw": { + "ko": "브랜드 관리 권한", + "en": "Brand management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 36, + "parent_id": 35, + "identifier": "sirsoft-ecommerce.brands.read", + "name": "브랜드 조회", + "name_raw": { + "ko": "브랜드 조회", + "en": "Read Brands" + }, + "description": "브랜드 목록 및 상세 조회", + "description_raw": { + "ko": "브랜드 목록 및 상세 조회", + "en": "Read brand list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "brand", + "owner_key": "created_by", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 37, + "parent_id": 35, + "identifier": "sirsoft-ecommerce.brands.create", + "name": "브랜드 생성", + "name_raw": { + "ko": "브랜드 생성", + "en": "Create Brand" + }, + "description": "브랜드 생성", + "description_raw": { + "ko": "브랜드 생성", + "en": "Create brand" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 38, + "parent_id": 35, + "identifier": "sirsoft-ecommerce.brands.update", + "name": "브랜드 수정", + "name_raw": { + "ko": "브랜드 수정", + "en": "Update Brand" + }, + "description": "브랜드 수정", + "description_raw": { + "ko": "브랜드 수정", + "en": "Update brand" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "brand", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 39, + "parent_id": 35, + "identifier": "sirsoft-ecommerce.brands.delete", + "name": "브랜드 삭제", + "name_raw": { + "ko": "브랜드 삭제", + "en": "Delete Brand" + }, + "description": "브랜드 삭제", + "description_raw": { + "ko": "브랜드 삭제", + "en": "Delete brand" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "brand", + "owner_key": "created_by", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 40, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.product-notice-templates", + "name": "상품정보제공고시 관리", + "name_raw": { + "ko": "상품정보제공고시 관리", + "en": "Product Notice Template Management" + }, + "description": "상품정보제공고시 템플릿 관리 권한", + "description_raw": { + "ko": "상품정보제공고시 템플릿 관리 권한", + "en": "Product notice template management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 5, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 41, + "parent_id": 40, + "identifier": "sirsoft-ecommerce.product-notice-templates.read", + "name": "조회", + "name_raw": { + "ko": "조회", + "en": "Read" + }, + "description": "상품정보제공고시 조회", + "description_raw": { + "ko": "상품정보제공고시 조회", + "en": "Read product notice templates" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 42, + "parent_id": 40, + "identifier": "sirsoft-ecommerce.product-notice-templates.create", + "name": "생성", + "name_raw": { + "ko": "생성", + "en": "Create" + }, + "description": "상품정보제공고시 생성", + "description_raw": { + "ko": "상품정보제공고시 생성", + "en": "Create product notice template" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 43, + "parent_id": 40, + "identifier": "sirsoft-ecommerce.product-notice-templates.update", + "name": "수정", + "name_raw": { + "ko": "수정", + "en": "Update" + }, + "description": "상품정보제공고시 수정", + "description_raw": { + "ko": "상품정보제공고시 수정", + "en": "Update product notice template" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 44, + "parent_id": 40, + "identifier": "sirsoft-ecommerce.product-notice-templates.delete", + "name": "삭제", + "name_raw": { + "ko": "삭제", + "en": "Delete" + }, + "description": "상품정보제공고시 삭제", + "description_raw": { + "ko": "상품정보제공고시 삭제", + "en": "Delete product notice template" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 45, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.product-common-infos", + "name": "공통정보 관리", + "name_raw": { + "ko": "공통정보 관리", + "en": "Product Common Info Management" + }, + "description": "상품 공통정보 관리 권한", + "description_raw": { + "ko": "상품 공통정보 관리 권한", + "en": "Product common info management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 6, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 46, + "parent_id": 45, + "identifier": "sirsoft-ecommerce.product-common-infos.read", + "name": "조회", + "name_raw": { + "ko": "조회", + "en": "Read" + }, + "description": "공통정보 조회", + "description_raw": { + "ko": "공통정보 조회", + "en": "Read product common info" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 47, + "parent_id": 45, + "identifier": "sirsoft-ecommerce.product-common-infos.create", + "name": "생성", + "name_raw": { + "ko": "생성", + "en": "Create" + }, + "description": "공통정보 생성", + "description_raw": { + "ko": "공통정보 생성", + "en": "Create product common info" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 48, + "parent_id": 45, + "identifier": "sirsoft-ecommerce.product-common-infos.update", + "name": "수정", + "name_raw": { + "ko": "수정", + "en": "Update" + }, + "description": "공통정보 수정", + "description_raw": { + "ko": "공통정보 수정", + "en": "Update product common info" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 49, + "parent_id": 45, + "identifier": "sirsoft-ecommerce.product-common-infos.delete", + "name": "삭제", + "name_raw": { + "ko": "삭제", + "en": "Delete" + }, + "description": "공통정보 삭제", + "description_raw": { + "ko": "공통정보 삭제", + "en": "Delete product common info" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 50, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.settings", + "name": "환경설정", + "name_raw": { + "ko": "환경설정", + "en": "Settings" + }, + "description": "이커머스 환경설정 권한", + "description_raw": { + "ko": "이커머스 환경설정 권한", + "en": "Ecommerce settings permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 7, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 51, + "parent_id": 50, + "identifier": "sirsoft-ecommerce.settings.read", + "name": "환경설정 조회", + "name_raw": { + "ko": "환경설정 조회", + "en": "View Settings" + }, + "description": "이커머스 환경설정 조회", + "description_raw": { + "ko": "이커머스 환경설정 조회", + "en": "View ecommerce settings" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 52, + "parent_id": 50, + "identifier": "sirsoft-ecommerce.settings.update", + "name": "환경설정 수정", + "name_raw": { + "ko": "환경설정 수정", + "en": "Update Settings" + }, + "description": "이커머스 환경설정 수정", + "description_raw": { + "ko": "이커머스 환경설정 수정", + "en": "Update ecommerce settings" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 53, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.promotion-coupon", + "name": "쿠폰 관리", + "name_raw": { + "ko": "쿠폰 관리", + "en": "Coupon Management" + }, + "description": "쿠폰 관리 권한", + "description_raw": { + "ko": "쿠폰 관리 권한", + "en": "Coupon management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 54, + "parent_id": 53, + "identifier": "sirsoft-ecommerce.promotion-coupon.read", + "name": "쿠폰 조회", + "name_raw": { + "ko": "쿠폰 조회", + "en": "Read Coupons" + }, + "description": "쿠폰 목록 및 상세 조회", + "description_raw": { + "ko": "쿠폰 목록 및 상세 조회", + "en": "Read coupon list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "coupon", + "owner_key": "created_by", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 55, + "parent_id": 53, + "identifier": "sirsoft-ecommerce.promotion-coupon.create", + "name": "쿠폰 생성", + "name_raw": { + "ko": "쿠폰 생성", + "en": "Create Coupon" + }, + "description": "새 쿠폰 등록", + "description_raw": { + "ko": "새 쿠폰 등록", + "en": "Create new coupon" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 56, + "parent_id": 53, + "identifier": "sirsoft-ecommerce.promotion-coupon.update", + "name": "쿠폰 수정", + "name_raw": { + "ko": "쿠폰 수정", + "en": "Update Coupon" + }, + "description": "쿠폰 정보 수정", + "description_raw": { + "ko": "쿠폰 정보 수정", + "en": "Update coupon information" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "coupon", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 57, + "parent_id": 53, + "identifier": "sirsoft-ecommerce.promotion-coupon.delete", + "name": "쿠폰 삭제", + "name_raw": { + "ko": "쿠폰 삭제", + "en": "Delete Coupon" + }, + "description": "쿠폰 삭제", + "description_raw": { + "ko": "쿠폰 삭제", + "en": "Delete coupon" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "coupon", + "owner_key": "created_by", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 58, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.shipping-policies", + "name": "배송정책 관리", + "name_raw": { + "ko": "배송정책 관리", + "en": "Shipping Policy Management" + }, + "description": "배송정책 관리 권한", + "description_raw": { + "ko": "배송정책 관리 권한", + "en": "Shipping policy management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 9, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 59, + "parent_id": 58, + "identifier": "sirsoft-ecommerce.shipping-policies.read", + "name": "배송정책 조회", + "name_raw": { + "ko": "배송정책 조회", + "en": "Read Shipping Policies" + }, + "description": "배송정책 목록 및 상세 조회", + "description_raw": { + "ko": "배송정책 목록 및 상세 조회", + "en": "Read shipping policy list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "shippingPolicy", + "owner_key": "created_by", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 60, + "parent_id": 58, + "identifier": "sirsoft-ecommerce.shipping-policies.create", + "name": "배송정책 생성", + "name_raw": { + "ko": "배송정책 생성", + "en": "Create Shipping Policy" + }, + "description": "새 배송정책 등록", + "description_raw": { + "ko": "새 배송정책 등록", + "en": "Create new shipping policy" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 61, + "parent_id": 58, + "identifier": "sirsoft-ecommerce.shipping-policies.update", + "name": "배송정책 수정", + "name_raw": { + "ko": "배송정책 수정", + "en": "Update Shipping Policy" + }, + "description": "배송정책 정보 수정", + "description_raw": { + "ko": "배송정책 정보 수정", + "en": "Update shipping policy information" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "shippingPolicy", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 62, + "parent_id": 58, + "identifier": "sirsoft-ecommerce.shipping-policies.delete", + "name": "배송정책 삭제", + "name_raw": { + "ko": "배송정책 삭제", + "en": "Delete Shipping Policy" + }, + "description": "배송정책 삭제", + "description_raw": { + "ko": "배송정책 삭제", + "en": "Delete shipping policy" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "shippingPolicy", + "owner_key": "created_by", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 63, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.product-labels", + "name": "상품 라벨 관리", + "name_raw": { + "ko": "상품 라벨 관리", + "en": "Product Label Management" + }, + "description": "상품 라벨 관리 권한", + "description_raw": { + "ko": "상품 라벨 관리 권한", + "en": "Product label management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 10, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 64, + "parent_id": 63, + "identifier": "sirsoft-ecommerce.product-labels.read", + "name": "상품 라벨 조회", + "name_raw": { + "ko": "상품 라벨 조회", + "en": "Read Product Labels" + }, + "description": "상품 라벨 목록 및 상세 조회", + "description_raw": { + "ko": "상품 라벨 목록 및 상세 조회", + "en": "Read product label list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 65, + "parent_id": 63, + "identifier": "sirsoft-ecommerce.product-labels.create", + "name": "상품 라벨 생성", + "name_raw": { + "ko": "상품 라벨 생성", + "en": "Create Product Label" + }, + "description": "새 상품 라벨 등록", + "description_raw": { + "ko": "새 상품 라벨 등록", + "en": "Create new product label" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 66, + "parent_id": 63, + "identifier": "sirsoft-ecommerce.product-labels.update", + "name": "상품 라벨 수정", + "name_raw": { + "ko": "상품 라벨 수정", + "en": "Update Product Label" + }, + "description": "상품 라벨 정보 수정", + "description_raw": { + "ko": "상품 라벨 정보 수정", + "en": "Update product label information" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 67, + "parent_id": 63, + "identifier": "sirsoft-ecommerce.product-labels.delete", + "name": "상품 라벨 삭제", + "name_raw": { + "ko": "상품 라벨 삭제", + "en": "Delete Product Label" + }, + "description": "상품 라벨 삭제", + "description_raw": { + "ko": "상품 라벨 삭제", + "en": "Delete product label" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 71, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.reviews", + "name": "리뷰 관리", + "name_raw": { + "ko": "리뷰 관리", + "en": "Review Management" + }, + "description": "상품 리뷰 관리 권한", + "description_raw": { + "ko": "상품 리뷰 관리 권한", + "en": "Product review management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 12, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 72, + "parent_id": 71, + "identifier": "sirsoft-ecommerce.reviews.read", + "name": "리뷰 조회", + "name_raw": { + "ko": "리뷰 조회", + "en": "Read Reviews" + }, + "description": "리뷰 목록 및 상세 조회", + "description_raw": { + "ko": "리뷰 목록 및 상세 조회", + "en": "Read review list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "review", + "owner_key": "user_id", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 73, + "parent_id": 71, + "identifier": "sirsoft-ecommerce.reviews.update", + "name": "리뷰 처리", + "name_raw": { + "ko": "리뷰 처리", + "en": "Manage Review" + }, + "description": "리뷰 상태 변경, 답변 등록/수정/삭제, 일괄 처리", + "description_raw": { + "ko": "리뷰 상태 변경, 답변 등록/수정/삭제, 일괄 처리", + "en": "Update review status, manage replies, bulk actions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "review", + "owner_key": "user_id", + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 74, + "parent_id": 71, + "identifier": "sirsoft-ecommerce.reviews.delete", + "name": "리뷰 삭제", + "name_raw": { + "ko": "리뷰 삭제", + "en": "Delete Review" + }, + "description": "리뷰 삭제", + "description_raw": { + "ko": "리뷰 삭제", + "en": "Delete review" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "review", + "owner_key": "user_id", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 75, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.inquiries", + "name": "문의 관리", + "name_raw": { + "ko": "문의 관리", + "en": "Inquiry Management" + }, + "description": "상품 1:1 문의 관리 권한", + "description_raw": { + "ko": "상품 1:1 문의 관리 권한", + "en": "Product inquiry management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 13, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 76, + "parent_id": 75, + "identifier": "sirsoft-ecommerce.inquiries.update", + "name": "문의 처리", + "name_raw": { + "ko": "문의 처리", + "en": "Manage Inquiry" + }, + "description": "답변 등록/수정/삭제, 비밀글 내용 열람", + "description_raw": { + "ko": "답변 등록/수정/삭제, 비밀글 내용 열람", + "en": "Create, update, and delete inquiry replies; view secret inquiry contents" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 77, + "parent_id": 75, + "identifier": "sirsoft-ecommerce.inquiries.delete", + "name": "문의 삭제", + "name_raw": { + "ko": "문의 삭제", + "en": "Delete Inquiry" + }, + "description": "고객이 작성한 문의 자체를 삭제", + "description_raw": { + "ko": "고객이 작성한 문의 자체를 삭제", + "en": "Delete inquiries submitted by customers" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 78, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.dashboard", + "name": "대시보드", + "name_raw": { + "ko": "대시보드", + "en": "Dashboard" + }, + "description": "관리자 대시보드 이커머스 영역 조회 권한", + "description_raw": { + "ko": "관리자 대시보드 이커머스 영역 조회 권한", + "en": "Admin dashboard commerce area view permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 14, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 79, + "parent_id": 78, + "identifier": "sirsoft-ecommerce.dashboard.view", + "name": "대시보드 조회", + "name_raw": { + "ko": "대시보드 조회", + "en": "View Dashboard" + }, + "description": "관리자 대시보드의 이커머스 판매 현황/리뷰/문의 위젯 조회", + "description_raw": { + "ko": "관리자 대시보드의 이커머스 판매 현황/리뷰/문의 위젯 조회", + "en": "View commerce sales/review/inquiry widgets on the admin dashboard" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 80, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-products", + "name": "사용자 상품", + "name_raw": { + "ko": "사용자 상품", + "en": "User Products" + }, + "description": "사용자 상품 접근 권한 (블랙컨슈머 차단용)", + "description_raw": { + "ko": "사용자 상품 접근 권한 (블랙컨슈머 차단용)", + "en": "User product access permissions (for blocking malicious consumers)" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 15, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 81, + "parent_id": 80, + "identifier": "sirsoft-ecommerce.user-products.read", + "name": "상품 조회", + "name_raw": { + "ko": "상품 조회", + "en": "View Products" + }, + "description": "상품 목록 및 상세 조회", + "description_raw": { + "ko": "상품 목록 및 상세 조회", + "en": "View product list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 82, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-orders", + "name": "사용자 주문", + "name_raw": { + "ko": "사용자 주문", + "en": "User Orders" + }, + "description": "사용자 주문 관련 권한 (블랙컨슈머 차단용)", + "description_raw": { + "ko": "사용자 주문 관련 권한 (블랙컨슈머 차단용)", + "en": "User order permissions (for blocking malicious consumers)" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 16, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 83, + "parent_id": 82, + "identifier": "sirsoft-ecommerce.user-orders.create", + "name": "주문하기", + "name_raw": { + "ko": "주문하기", + "en": "Create Order" + }, + "description": "주문 생성", + "description_raw": { + "ko": "주문 생성", + "en": "Create a new order" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 84, + "parent_id": 82, + "identifier": "sirsoft-ecommerce.user-orders.cancel", + "name": "주문 취소", + "name_raw": { + "ko": "주문 취소", + "en": "Cancel Order" + }, + "description": "주문 취소 요청", + "description_raw": { + "ko": "주문 취소 요청", + "en": "Request order cancellation" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 85, + "parent_id": 82, + "identifier": "sirsoft-ecommerce.user-orders.confirm", + "name": "구매확정", + "name_raw": { + "ko": "구매확정", + "en": "Confirm Purchase" + }, + "description": "주문 상품 구매확정", + "description_raw": { + "ko": "주문 상품 구매확정", + "en": "Confirm purchase of order item" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 86, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-reviews", + "name": "사용자 리뷰", + "name_raw": { + "ko": "사용자 리뷰", + "en": "User Reviews" + }, + "description": "사용자 리뷰 관련 권한 (블랙컨슈머 차단용)", + "description_raw": { + "ko": "사용자 리뷰 관련 권한 (블랙컨슈머 차단용)", + "en": "User review permissions (for blocking malicious consumers)" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 17, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 87, + "parent_id": 86, + "identifier": "sirsoft-ecommerce.user-reviews.write", + "name": "리뷰 작성", + "name_raw": { + "ko": "리뷰 작성", + "en": "Write Review" + }, + "description": "상품 리뷰 작성", + "description_raw": { + "ko": "상품 리뷰 작성", + "en": "Write product review" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 88, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.mileage", + "name": "마일리지 관리", + "name_raw": { + "ko": "마일리지 관리", + "en": "Mileage Management" + }, + "description": "마일리지 내역 조회 및 수동 지급/차감 권한", + "description_raw": { + "ko": "마일리지 내역 조회 및 수동 지급/차감 권한", + "en": "Mileage history and manual grant/deduct permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 18, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 89, + "parent_id": 88, + "identifier": "sirsoft-ecommerce.mileage.read", + "name": "마일리지 내역 조회", + "name_raw": { + "ko": "마일리지 내역 조회", + "en": "Read Mileage" + }, + "description": "마일리지 내역 목록 및 상세 조회", + "description_raw": { + "ko": "마일리지 내역 목록 및 상세 조회", + "en": "Read mileage history list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "mileage-transaction", + "owner_key": "user_id", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 90, + "parent_id": 88, + "identifier": "sirsoft-ecommerce.mileage.manage", + "name": "마일리지 수동 처리", + "name_raw": { + "ko": "마일리지 수동 처리", + "en": "Manage Mileage" + }, + "description": "마일리지 수동 지급/차감 및 일괄 유효기간 연장", + "description_raw": { + "ko": "마일리지 수동 지급/차감 및 일괄 유효기간 연장", + "en": "Manual grant/deduct and bulk expiry extension" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "mileage-transaction", + "owner_key": "user_id", + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 91, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-currency", + "name": "회원 결제 통화 관리", + "name_raw": { + "ko": "회원 결제 통화 관리", + "en": "User Currency Management" + }, + "description": "관리자가 회원별 결제 통화를 변경하는 권한", + "description_raw": { + "ko": "관리자가 회원별 결제 통화를 변경하는 권한", + "en": "Permission for admins to change a user's payment currency" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 19, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 92, + "parent_id": 91, + "identifier": "sirsoft-ecommerce.user-currency.manage", + "name": "회원 결제 통화 변경", + "name_raw": { + "ko": "회원 결제 통화 변경", + "en": "Manage User Currency" + }, + "description": "회원별 결제 통화 변경", + "description_raw": { + "ko": "회원별 결제 통화 변경", + "en": "Change a user's payment currency" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 93, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-shipping-country", + "name": "회원 배송국가 관리", + "name_raw": { + "ko": "회원 배송국가 관리", + "en": "User Shipping Country Management" + }, + "description": "관리자가 회원별 배송국가를 변경하는 권한", + "description_raw": { + "ko": "관리자가 회원별 배송국가를 변경하는 권한", + "en": "Permission for admins to change a user's shipping country" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 20, + "is_assigned": false, + "is_assignable": false, + "children": [ + { + "id": 94, + "parent_id": 93, + "identifier": "sirsoft-ecommerce.user-shipping-country.manage", + "name": "회원 배송국가 변경", + "name_raw": { + "ko": "회원 배송국가 변경", + "en": "Manage User Shipping Country" + }, + "description": "회원별 배송국가 변경", + "description_raw": { + "ko": "회원별 배송국가 변경", + "en": "Change a user's shipping country" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + } + ] + } + ], + "created_at": "2026-07-08 10:43:31", + "updated_at": "2026-07-08 10:43:31", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_toggle_status": true + } + }, + "... (총 6건 중 2건 표시)" + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 6, + "from": 1, + "to": 6, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -90,9 +2667,88 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.role.store_validation_rules`). +**요청 예시** + +```http +POST /api/admin/roles HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "identifier": "example-key", + "name": "예시 이름", + "description": "예시 내용입니다.", + "is_active": true, + "permissions": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `14` | 기본 키 (내부 식별자) | +| identifier | string | `probe_6a4dc0a720350` | 역할명 (예: admin, user, manager) | +| name | string | `실측 예시값` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| name_raw | object | `{"ko":"실측 예시값","en":"실측 예시값","fr":"실측 예시값"}` | `name` 의 원본 값 (현재 로케일 미해석 원본 JSON/문자열) | +| description | string | `실측 예시값` | 설명 (다국어 필드는 로케일별 값 객체) | +| description_raw | object | `{"ko":"실측 예시값","en":"실측 예시값","fr":"실측 예시값"}` | `description` 의 원본 값 (현재 로케일 미해석 원본 JSON/문자열) | +| extension_type | null | `null` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) | +| extension_identifier | null | `null` | 이 리소스를 소유한 확장의 식별자 | +| extension_name | null | `null` | 이 리소스를 소유한 확장의 표시 이름 (manifest name) | +| is_deletable | boolean | `true` | deletable 여부 | +| is_active | boolean | `true` | active 여부 | +| created_at | string | `2026-07-08 12:14:47` | 생성 일시 | +| updated_at | string | `2026-07-08 12:14:47` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 201 +``` + +```json +{ + "success": true, + "message": "역할이 성공적으로 생성되었습니다.", + "data": { + "id": 14, + "identifier": "probe_6a4dc0a720350", + "name": "실측 예시값", + "name_raw": { + "ko": "실측 예시값", + "en": "실측 예시값", + "fr": "실측 예시값" + }, + "description": "실측 예시값", + "description_raw": { + "ko": "실측 예시값", + "en": "실측 예시값", + "fr": "실측 예시값" + }, + "extension_type": null, + "extension_identifier": null, + "extension_name": null, + "is_deletable": true, + "is_active": true, + "created_at": "2026-07-08 12:14:47", + "updated_at": "2026-07-08 12:14:47", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_toggle_status": true + } + } +} +``` **에러 응답** @@ -119,6 +2775,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/roles/active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -140,6 +2805,81 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | updated_at | string | `2026-06-30 13:41:48` | 최종 수정 일시 | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "역할 정보를 성공적으로 가져왔습니다.", + "data": { + "data": [ + { + "id": 1, + "identifier": "admin", + "name": "관리자", + "name_raw": { + "ko": "관리자", + "en": "Administrator" + }, + "description": "시스템의 모든 기능에 접근할 수 있는 최고 관리자입니다.", + "description_raw": { + "ko": "시스템의 모든 기능에 접근할 수 있는 최고 관리자입니다.", + "en": "Super administrator with access to all system features." + }, + "extension_type": "core", + "extension_identifier": "core", + "extension_name": null, + "is_deletable": false, + "is_active": true, + "created_at": "2026-07-07 16:38:39", + "updated_at": "2026-07-08 10:47:51", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_toggle_status": true + } + }, + { + "id": 2, + "identifier": "user", + "name": "일반 사용자", + "name_raw": { + "ko": "일반 사용자", + "en": "User" + }, + "description": "기본 사용자 역할입니다.", + "description_raw": { + "ko": "기본 사용자 역할입니다.", + "en": "Default user role." + }, + "extension_type": "core", + "extension_identifier": "core", + "extension_name": null, + "is_deletable": false, + "is_active": true, + "created_at": "2026-07-07 16:38:39", + "updated_at": "2026-07-08 10:47:51", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_toggle_status": true + } + }, + "... (총 6건 중 2건 표시)" + ], + "abilities": { + "can_assign_roles": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -165,9 +2905,34 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | role | path | string | 예 | — | 대상 role의 식별자 | +**요청 예시** + +```http +DELETE /api/admin/roles/4 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "역할이 성공적으로 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -196,6 +2961,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | role | path | string | 예 | — | 대상 role의 식별자 | +**요청 예시** + +```http +GET /api/admin/roles/4 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -221,6 +2995,6258 @@ _단건 응답: `data` 객체의 필드._ | updated_at | string | `2026-07-06 19:15:16` | 최종 수정 일시 | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "역할 정보를 성공적으로 가져왔습니다.", + "data": { + "id": 4, + "identifier": "apidoc-sample-role", + "name": "API 문서 샘플 역할", + "name_raw": { + "ko": "API 문서 샘플 역할", + "en": "API Doc Sample Role" + }, + "description": "문서 실측용 역할", + "description_raw": { + "ko": "문서 실측용 역할", + "en": "Sample role for API docs" + }, + "extension_type": null, + "extension_identifier": null, + "extension_name": null, + "is_deletable": true, + "is_active": true, + "users_count": 1, + "permission_ids": [ + 1, + 2, + 3, + 4, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 157, + 158, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198 + ], + "permission_values": [ + { + "id": 1, + "scope_type": null + }, + { + "id": 2, + "scope_type": null + }, + { + "id": 3, + "scope_type": null + }, + { + "id": 4, + "scope_type": null + }, + { + "id": 21, + "scope_type": null + }, + { + "id": 22, + "scope_type": null + }, + { + "id": 23, + "scope_type": null + }, + { + "id": 24, + "scope_type": null + }, + { + "id": 25, + "scope_type": null + }, + { + "id": 26, + "scope_type": null + }, + { + "id": 27, + "scope_type": null + }, + { + "id": 28, + "scope_type": null + }, + { + "id": 29, + "scope_type": null + }, + { + "id": 30, + "scope_type": null + }, + { + "id": 31, + "scope_type": null + }, + { + "id": 32, + "scope_type": null + }, + { + "id": 33, + "scope_type": null + }, + { + "id": 34, + "scope_type": null + }, + { + "id": 35, + "scope_type": null + }, + { + "id": 36, + "scope_type": null + }, + { + "id": 37, + "scope_type": null + }, + { + "id": 38, + "scope_type": null + }, + { + "id": 39, + "scope_type": null + }, + { + "id": 40, + "scope_type": null + }, + { + "id": 41, + "scope_type": null + }, + { + "id": 42, + "scope_type": null + }, + { + "id": 43, + "scope_type": null + }, + { + "id": 44, + "scope_type": null + }, + { + "id": 45, + "scope_type": null + }, + { + "id": 46, + "scope_type": null + }, + { + "id": 47, + "scope_type": null + }, + { + "id": 48, + "scope_type": null + }, + { + "id": 49, + "scope_type": null + }, + { + "id": 50, + "scope_type": null + }, + { + "id": 51, + "scope_type": null + }, + { + "id": 52, + "scope_type": null + }, + { + "id": 53, + "scope_type": null + }, + { + "id": 54, + "scope_type": null + }, + { + "id": 55, + "scope_type": null + }, + { + "id": 56, + "scope_type": null + }, + { + "id": 57, + "scope_type": null + }, + { + "id": 58, + "scope_type": null + }, + { + "id": 59, + "scope_type": null + }, + { + "id": 60, + "scope_type": null + }, + { + "id": 61, + "scope_type": null + }, + { + "id": 62, + "scope_type": null + }, + { + "id": 63, + "scope_type": null + }, + { + "id": 64, + "scope_type": null + }, + { + "id": 65, + "scope_type": null + }, + { + "id": 66, + "scope_type": null + }, + { + "id": 67, + "scope_type": null + }, + { + "id": 68, + "scope_type": null + }, + { + "id": 69, + "scope_type": null + }, + { + "id": 70, + "scope_type": null + }, + { + "id": 71, + "scope_type": null + }, + { + "id": 72, + "scope_type": null + }, + { + "id": 73, + "scope_type": null + }, + { + "id": 74, + "scope_type": null + }, + { + "id": 75, + "scope_type": null + }, + { + "id": 76, + "scope_type": null + }, + { + "id": 77, + "scope_type": null + }, + { + "id": 78, + "scope_type": null + }, + { + "id": 79, + "scope_type": null + }, + { + "id": 80, + "scope_type": null + }, + { + "id": 81, + "scope_type": null + }, + { + "id": 82, + "scope_type": null + }, + { + "id": 83, + "scope_type": null + }, + { + "id": 84, + "scope_type": null + }, + { + "id": 85, + "scope_type": null + }, + { + "id": 86, + "scope_type": null + }, + { + "id": 87, + "scope_type": null + }, + { + "id": 88, + "scope_type": null + }, + { + "id": 89, + "scope_type": null + }, + { + "id": 90, + "scope_type": null + }, + { + "id": 91, + "scope_type": null + }, + { + "id": 92, + "scope_type": null + }, + { + "id": 93, + "scope_type": null + }, + { + "id": 94, + "scope_type": null + }, + { + "id": 95, + "scope_type": null + }, + { + "id": 96, + "scope_type": null + }, + { + "id": 97, + "scope_type": null + }, + { + "id": 98, + "scope_type": null + }, + { + "id": 99, + "scope_type": null + }, + { + "id": 100, + "scope_type": null + }, + { + "id": 101, + "scope_type": null + }, + { + "id": 102, + "scope_type": null + }, + { + "id": 103, + "scope_type": null + }, + { + "id": 104, + "scope_type": null + }, + { + "id": 105, + "scope_type": null + }, + { + "id": 106, + "scope_type": null + }, + { + "id": 107, + "scope_type": null + }, + { + "id": 108, + "scope_type": null + }, + { + "id": 109, + "scope_type": null + }, + { + "id": 110, + "scope_type": null + }, + { + "id": 111, + "scope_type": null + }, + { + "id": 112, + "scope_type": null + }, + { + "id": 113, + "scope_type": null + }, + { + "id": 114, + "scope_type": null + }, + { + "id": 115, + "scope_type": null + }, + { + "id": 116, + "scope_type": null + }, + { + "id": 117, + "scope_type": null + }, + { + "id": 118, + "scope_type": null + }, + { + "id": 119, + "scope_type": null + }, + { + "id": 120, + "scope_type": null + }, + { + "id": 121, + "scope_type": null + }, + { + "id": 122, + "scope_type": null + }, + { + "id": 123, + "scope_type": null + }, + { + "id": 124, + "scope_type": null + }, + { + "id": 125, + "scope_type": null + }, + { + "id": 126, + "scope_type": null + }, + { + "id": 127, + "scope_type": null + }, + { + "id": 128, + "scope_type": null + }, + { + "id": 129, + "scope_type": null + }, + { + "id": 130, + "scope_type": null + }, + { + "id": 131, + "scope_type": null + }, + { + "id": 132, + "scope_type": null + }, + { + "id": 133, + "scope_type": null + }, + { + "id": 134, + "scope_type": null + }, + { + "id": 135, + "scope_type": null + }, + { + "id": 136, + "scope_type": null + }, + { + "id": 137, + "scope_type": null + }, + { + "id": 138, + "scope_type": null + }, + { + "id": 139, + "scope_type": null + }, + { + "id": 140, + "scope_type": null + }, + { + "id": 141, + "scope_type": null + }, + { + "id": 142, + "scope_type": null + }, + { + "id": 143, + "scope_type": null + }, + { + "id": 144, + "scope_type": null + }, + { + "id": 145, + "scope_type": null + }, + { + "id": 146, + "scope_type": null + }, + { + "id": 147, + "scope_type": null + }, + { + "id": 148, + "scope_type": null + }, + { + "id": 149, + "scope_type": null + }, + { + "id": 150, + "scope_type": null + }, + { + "id": 151, + "scope_type": null + }, + { + "id": 152, + "scope_type": null + }, + { + "id": 153, + "scope_type": null + }, + { + "id": 154, + "scope_type": null + }, + { + "id": 155, + "scope_type": null + }, + { + "id": 156, + "scope_type": null + }, + { + "id": 157, + "scope_type": null + }, + { + "id": 158, + "scope_type": null + }, + { + "id": 159, + "scope_type": null + }, + { + "id": 160, + "scope_type": null + }, + { + "id": 161, + "scope_type": null + }, + { + "id": 162, + "scope_type": null + }, + { + "id": 163, + "scope_type": null + }, + { + "id": 164, + "scope_type": null + }, + { + "id": 165, + "scope_type": null + }, + { + "id": 166, + "scope_type": null + }, + { + "id": 167, + "scope_type": null + }, + { + "id": 168, + "scope_type": null + }, + { + "id": 169, + "scope_type": null + }, + { + "id": 170, + "scope_type": null + }, + { + "id": 171, + "scope_type": null + }, + { + "id": 172, + "scope_type": null + }, + { + "id": 173, + "scope_type": null + }, + { + "id": 174, + "scope_type": null + }, + { + "id": 175, + "scope_type": null + }, + { + "id": 176, + "scope_type": null + }, + { + "id": 177, + "scope_type": null + }, + { + "id": 178, + "scope_type": null + }, + { + "id": 179, + "scope_type": null + }, + { + "id": 180, + "scope_type": null + }, + { + "id": 181, + "scope_type": null + }, + { + "id": 182, + "scope_type": null + }, + { + "id": 183, + "scope_type": null + }, + { + "id": 184, + "scope_type": null + }, + { + "id": 185, + "scope_type": null + }, + { + "id": 186, + "scope_type": null + }, + { + "id": 187, + "scope_type": null + }, + { + "id": 188, + "scope_type": null + }, + { + "id": 189, + "scope_type": null + }, + { + "id": 190, + "scope_type": null + }, + { + "id": 191, + "scope_type": null + }, + { + "id": 192, + "scope_type": null + }, + { + "id": 193, + "scope_type": null + }, + { + "id": 194, + "scope_type": null + }, + { + "id": 195, + "scope_type": null + }, + { + "id": 196, + "scope_type": null + }, + { + "id": 197, + "scope_type": null + }, + { + "id": 198, + "scope_type": null + } + ], + "permissions": [ + { + "id": 3, + "parent_id": null, + "identifier": "sirsoft-board", + "name": "게시판", + "name_raw": { + "ko": "게시판", + "en": "Board" + }, + "description": "게시판 모듈 권한", + "description_raw": { + "ko": "게시판 모듈 권한", + "en": "Board module permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 100, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 95, + "parent_id": 3, + "identifier": "sirsoft-board.boards", + "name": "게시판 관리", + "name_raw": { + "ko": "게시판 관리", + "en": "Board Management" + }, + "description": "게시판 관리 권한", + "description_raw": { + "ko": "게시판 관리 권한", + "en": "Board management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 96, + "parent_id": 95, + "identifier": "sirsoft-board.boards.read", + "name": "게시판 조회", + "name_raw": { + "ko": "게시판 조회", + "en": "View Boards" + }, + "description": "게시판 목록 및 상세 조회", + "description_raw": { + "ko": "게시판 목록 및 상세 조회", + "en": "View board list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": "board", + "owner_key": "created_by", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 97, + "parent_id": 95, + "identifier": "sirsoft-board.boards.create", + "name": "게시판 생성", + "name_raw": { + "ko": "게시판 생성", + "en": "Create Board" + }, + "description": "새 게시판 생성", + "description_raw": { + "ko": "새 게시판 생성", + "en": "Create new board" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 98, + "parent_id": 95, + "identifier": "sirsoft-board.boards.update", + "name": "게시판 수정", + "name_raw": { + "ko": "게시판 수정", + "en": "Update Board" + }, + "description": "게시판 설정 수정", + "description_raw": { + "ko": "게시판 설정 수정", + "en": "Update board settings" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": "board", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 99, + "parent_id": 95, + "identifier": "sirsoft-board.boards.delete", + "name": "게시판 삭제", + "name_raw": { + "ko": "게시판 삭제", + "en": "Delete Board" + }, + "description": "게시판 삭제", + "description_raw": { + "ko": "게시판 삭제", + "en": "Delete board" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": "board", + "owner_key": "created_by", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 100, + "parent_id": 3, + "identifier": "sirsoft-board.settings", + "name": "환경설정", + "name_raw": { + "ko": "환경설정", + "en": "Settings" + }, + "description": "게시판 환경설정 권한", + "description_raw": { + "ko": "게시판 환경설정 권한", + "en": "Board settings permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 101, + "parent_id": 100, + "identifier": "sirsoft-board.settings.read", + "name": "환경설정 조회", + "name_raw": { + "ko": "환경설정 조회", + "en": "View Settings" + }, + "description": "게시판 환경설정 조회", + "description_raw": { + "ko": "게시판 환경설정 조회", + "en": "View board settings" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 102, + "parent_id": 100, + "identifier": "sirsoft-board.settings.update", + "name": "환경설정 수정", + "name_raw": { + "ko": "환경설정 수정", + "en": "Update Settings" + }, + "description": "게시판 환경설정 수정", + "description_raw": { + "ko": "게시판 환경설정 수정", + "en": "Update board settings" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 103, + "parent_id": 3, + "identifier": "sirsoft-board.identity.policies", + "name": "게시판 본인인증 정책", + "name_raw": { + "ko": "게시판 본인인증 정책", + "en": "Board Identity Policies" + }, + "description": "게시판 컨텍스트의 본인인증 정책 관리 권한", + "description_raw": { + "ko": "게시판 컨텍스트의 본인인증 정책 관리 권한", + "en": "Manage identity verification policies in board context" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 104, + "parent_id": 103, + "identifier": "sirsoft-board.identity.policies.read", + "name": "본인인증 정책 조회", + "name_raw": { + "ko": "본인인증 정책 조회", + "en": "View Identity Policies" + }, + "description": "게시판 본인인증 정책 조회", + "description_raw": { + "ko": "게시판 본인인증 정책 조회", + "en": "View board identity policies" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 105, + "parent_id": 103, + "identifier": "sirsoft-board.identity.policies.update", + "name": "본인인증 정책 수정", + "name_raw": { + "ko": "본인인증 정책 수정", + "en": "Update Identity Policies" + }, + "description": "게시판 본인인증 정책 수정/추가/삭제", + "description_raw": { + "ko": "게시판 본인인증 정책 수정/추가/삭제", + "en": "Update, add, delete board identity policies" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 106, + "parent_id": 3, + "identifier": "sirsoft-board.reports", + "name": "게시판 신고 관리", + "name_raw": { + "ko": "게시판 신고 관리", + "en": "Report Management" + }, + "description": "게시판 신고 관리 권한", + "description_raw": { + "ko": "게시판 신고 관리 권한", + "en": "Board report management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 107, + "parent_id": 106, + "identifier": "sirsoft-board.reports.view", + "name": "신고 조회", + "name_raw": { + "ko": "신고 조회", + "en": "View Reports" + }, + "description": "신고 목록 및 상세 조회", + "description_raw": { + "ko": "신고 목록 및 상세 조회", + "en": "View report list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": "report", + "owner_key": "reporter_id", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 108, + "parent_id": 106, + "identifier": "sirsoft-board.reports.manage", + "name": "신고 처리", + "name_raw": { + "ko": "신고 처리", + "en": "Manage Reports" + }, + "description": "신고 상태 변경 및 처리", + "description_raw": { + "ko": "신고 상태 변경 및 처리", + "en": "Update report status and process" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": "report", + "owner_key": "reporter_id", + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + } + ] + }, + { + "id": 21, + "parent_id": null, + "identifier": "sirsoft-ecommerce", + "name": "이커머스", + "name_raw": { + "ko": "이커머스", + "en": "Ecommerce" + }, + "description": "이커머스 모듈 권한", + "description_raw": { + "ko": "이커머스 모듈 권한", + "en": "Ecommerce module permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 100, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 22, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.products", + "name": "상품 관리", + "name_raw": { + "ko": "상품 관리", + "en": "Product Management" + }, + "description": "상품 관리 권한", + "description_raw": { + "ko": "상품 관리 권한", + "en": "Product management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 23, + "parent_id": 22, + "identifier": "sirsoft-ecommerce.products.read", + "name": "상품 조회", + "name_raw": { + "ko": "상품 조회", + "en": "Read Products" + }, + "description": "상품 목록 및 상세 조회", + "description_raw": { + "ko": "상품 목록 및 상세 조회", + "en": "Read product list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "product", + "owner_key": "created_by", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 24, + "parent_id": 22, + "identifier": "sirsoft-ecommerce.products.create", + "name": "상품 생성", + "name_raw": { + "ko": "상품 생성", + "en": "Create Product" + }, + "description": "새 상품 등록", + "description_raw": { + "ko": "새 상품 등록", + "en": "Create new product" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 25, + "parent_id": 22, + "identifier": "sirsoft-ecommerce.products.update", + "name": "상품 수정", + "name_raw": { + "ko": "상품 수정", + "en": "Update Product" + }, + "description": "상품 정보 수정", + "description_raw": { + "ko": "상품 정보 수정", + "en": "Update product information" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "product", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 26, + "parent_id": 22, + "identifier": "sirsoft-ecommerce.products.delete", + "name": "상품 삭제", + "name_raw": { + "ko": "상품 삭제", + "en": "Delete Product" + }, + "description": "상품 삭제", + "description_raw": { + "ko": "상품 삭제", + "en": "Delete product" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "product", + "owner_key": "created_by", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 27, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.orders", + "name": "주문 관리", + "name_raw": { + "ko": "주문 관리", + "en": "Order Management" + }, + "description": "주문 관리 권한", + "description_raw": { + "ko": "주문 관리 권한", + "en": "Order management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 28, + "parent_id": 27, + "identifier": "sirsoft-ecommerce.orders.read", + "name": "주문 조회", + "name_raw": { + "ko": "주문 조회", + "en": "Read Orders" + }, + "description": "주문 목록 및 상세 조회", + "description_raw": { + "ko": "주문 목록 및 상세 조회", + "en": "Read order list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "order", + "owner_key": "user_id", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 29, + "parent_id": 27, + "identifier": "sirsoft-ecommerce.orders.update", + "name": "주문 수정", + "name_raw": { + "ko": "주문 수정", + "en": "Update Order" + }, + "description": "주문 상태 변경", + "description_raw": { + "ko": "주문 상태 변경", + "en": "Update order status" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "order", + "owner_key": "user_id", + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 30, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.categories", + "name": "카테고리 관리", + "name_raw": { + "ko": "카테고리 관리", + "en": "Category Management" + }, + "description": "카테고리 관리 권한", + "description_raw": { + "ko": "카테고리 관리 권한", + "en": "Category management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 31, + "parent_id": 30, + "identifier": "sirsoft-ecommerce.categories.read", + "name": "카테고리 조회", + "name_raw": { + "ko": "카테고리 조회", + "en": "Read Categories" + }, + "description": "카테고리 목록 및 상세 조회", + "description_raw": { + "ko": "카테고리 목록 및 상세 조회", + "en": "Read category list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 32, + "parent_id": 30, + "identifier": "sirsoft-ecommerce.categories.create", + "name": "카테고리 생성", + "name_raw": { + "ko": "카테고리 생성", + "en": "Create Category" + }, + "description": "카테고리 생성", + "description_raw": { + "ko": "카테고리 생성", + "en": "Create category" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 33, + "parent_id": 30, + "identifier": "sirsoft-ecommerce.categories.update", + "name": "카테고리 수정", + "name_raw": { + "ko": "카테고리 수정", + "en": "Update Category" + }, + "description": "카테고리 수정", + "description_raw": { + "ko": "카테고리 수정", + "en": "Update category" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 34, + "parent_id": 30, + "identifier": "sirsoft-ecommerce.categories.delete", + "name": "카테고리 삭제", + "name_raw": { + "ko": "카테고리 삭제", + "en": "Delete Category" + }, + "description": "카테고리 삭제", + "description_raw": { + "ko": "카테고리 삭제", + "en": "Delete category" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 35, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.brands", + "name": "브랜드 관리", + "name_raw": { + "ko": "브랜드 관리", + "en": "Brand Management" + }, + "description": "브랜드 관리 권한", + "description_raw": { + "ko": "브랜드 관리 권한", + "en": "Brand management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 36, + "parent_id": 35, + "identifier": "sirsoft-ecommerce.brands.read", + "name": "브랜드 조회", + "name_raw": { + "ko": "브랜드 조회", + "en": "Read Brands" + }, + "description": "브랜드 목록 및 상세 조회", + "description_raw": { + "ko": "브랜드 목록 및 상세 조회", + "en": "Read brand list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "brand", + "owner_key": "created_by", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 37, + "parent_id": 35, + "identifier": "sirsoft-ecommerce.brands.create", + "name": "브랜드 생성", + "name_raw": { + "ko": "브랜드 생성", + "en": "Create Brand" + }, + "description": "브랜드 생성", + "description_raw": { + "ko": "브랜드 생성", + "en": "Create brand" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 38, + "parent_id": 35, + "identifier": "sirsoft-ecommerce.brands.update", + "name": "브랜드 수정", + "name_raw": { + "ko": "브랜드 수정", + "en": "Update Brand" + }, + "description": "브랜드 수정", + "description_raw": { + "ko": "브랜드 수정", + "en": "Update brand" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "brand", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 39, + "parent_id": 35, + "identifier": "sirsoft-ecommerce.brands.delete", + "name": "브랜드 삭제", + "name_raw": { + "ko": "브랜드 삭제", + "en": "Delete Brand" + }, + "description": "브랜드 삭제", + "description_raw": { + "ko": "브랜드 삭제", + "en": "Delete brand" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "brand", + "owner_key": "created_by", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 40, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.product-notice-templates", + "name": "상품정보제공고시 관리", + "name_raw": { + "ko": "상품정보제공고시 관리", + "en": "Product Notice Template Management" + }, + "description": "상품정보제공고시 템플릿 관리 권한", + "description_raw": { + "ko": "상품정보제공고시 템플릿 관리 권한", + "en": "Product notice template management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 5, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 41, + "parent_id": 40, + "identifier": "sirsoft-ecommerce.product-notice-templates.read", + "name": "조회", + "name_raw": { + "ko": "조회", + "en": "Read" + }, + "description": "상품정보제공고시 조회", + "description_raw": { + "ko": "상품정보제공고시 조회", + "en": "Read product notice templates" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 42, + "parent_id": 40, + "identifier": "sirsoft-ecommerce.product-notice-templates.create", + "name": "생성", + "name_raw": { + "ko": "생성", + "en": "Create" + }, + "description": "상품정보제공고시 생성", + "description_raw": { + "ko": "상품정보제공고시 생성", + "en": "Create product notice template" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 43, + "parent_id": 40, + "identifier": "sirsoft-ecommerce.product-notice-templates.update", + "name": "수정", + "name_raw": { + "ko": "수정", + "en": "Update" + }, + "description": "상품정보제공고시 수정", + "description_raw": { + "ko": "상품정보제공고시 수정", + "en": "Update product notice template" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 44, + "parent_id": 40, + "identifier": "sirsoft-ecommerce.product-notice-templates.delete", + "name": "삭제", + "name_raw": { + "ko": "삭제", + "en": "Delete" + }, + "description": "상품정보제공고시 삭제", + "description_raw": { + "ko": "상품정보제공고시 삭제", + "en": "Delete product notice template" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 45, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.product-common-infos", + "name": "공통정보 관리", + "name_raw": { + "ko": "공통정보 관리", + "en": "Product Common Info Management" + }, + "description": "상품 공통정보 관리 권한", + "description_raw": { + "ko": "상품 공통정보 관리 권한", + "en": "Product common info management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 6, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 46, + "parent_id": 45, + "identifier": "sirsoft-ecommerce.product-common-infos.read", + "name": "조회", + "name_raw": { + "ko": "조회", + "en": "Read" + }, + "description": "공통정보 조회", + "description_raw": { + "ko": "공통정보 조회", + "en": "Read product common info" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 47, + "parent_id": 45, + "identifier": "sirsoft-ecommerce.product-common-infos.create", + "name": "생성", + "name_raw": { + "ko": "생성", + "en": "Create" + }, + "description": "공통정보 생성", + "description_raw": { + "ko": "공통정보 생성", + "en": "Create product common info" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 48, + "parent_id": 45, + "identifier": "sirsoft-ecommerce.product-common-infos.update", + "name": "수정", + "name_raw": { + "ko": "수정", + "en": "Update" + }, + "description": "공통정보 수정", + "description_raw": { + "ko": "공통정보 수정", + "en": "Update product common info" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 49, + "parent_id": 45, + "identifier": "sirsoft-ecommerce.product-common-infos.delete", + "name": "삭제", + "name_raw": { + "ko": "삭제", + "en": "Delete" + }, + "description": "공통정보 삭제", + "description_raw": { + "ko": "공통정보 삭제", + "en": "Delete product common info" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 50, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.settings", + "name": "환경설정", + "name_raw": { + "ko": "환경설정", + "en": "Settings" + }, + "description": "이커머스 환경설정 권한", + "description_raw": { + "ko": "이커머스 환경설정 권한", + "en": "Ecommerce settings permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 7, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 51, + "parent_id": 50, + "identifier": "sirsoft-ecommerce.settings.read", + "name": "환경설정 조회", + "name_raw": { + "ko": "환경설정 조회", + "en": "View Settings" + }, + "description": "이커머스 환경설정 조회", + "description_raw": { + "ko": "이커머스 환경설정 조회", + "en": "View ecommerce settings" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 52, + "parent_id": 50, + "identifier": "sirsoft-ecommerce.settings.update", + "name": "환경설정 수정", + "name_raw": { + "ko": "환경설정 수정", + "en": "Update Settings" + }, + "description": "이커머스 환경설정 수정", + "description_raw": { + "ko": "이커머스 환경설정 수정", + "en": "Update ecommerce settings" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 53, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.promotion-coupon", + "name": "쿠폰 관리", + "name_raw": { + "ko": "쿠폰 관리", + "en": "Coupon Management" + }, + "description": "쿠폰 관리 권한", + "description_raw": { + "ko": "쿠폰 관리 권한", + "en": "Coupon management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 54, + "parent_id": 53, + "identifier": "sirsoft-ecommerce.promotion-coupon.read", + "name": "쿠폰 조회", + "name_raw": { + "ko": "쿠폰 조회", + "en": "Read Coupons" + }, + "description": "쿠폰 목록 및 상세 조회", + "description_raw": { + "ko": "쿠폰 목록 및 상세 조회", + "en": "Read coupon list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "coupon", + "owner_key": "created_by", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 55, + "parent_id": 53, + "identifier": "sirsoft-ecommerce.promotion-coupon.create", + "name": "쿠폰 생성", + "name_raw": { + "ko": "쿠폰 생성", + "en": "Create Coupon" + }, + "description": "새 쿠폰 등록", + "description_raw": { + "ko": "새 쿠폰 등록", + "en": "Create new coupon" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 56, + "parent_id": 53, + "identifier": "sirsoft-ecommerce.promotion-coupon.update", + "name": "쿠폰 수정", + "name_raw": { + "ko": "쿠폰 수정", + "en": "Update Coupon" + }, + "description": "쿠폰 정보 수정", + "description_raw": { + "ko": "쿠폰 정보 수정", + "en": "Update coupon information" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "coupon", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 57, + "parent_id": 53, + "identifier": "sirsoft-ecommerce.promotion-coupon.delete", + "name": "쿠폰 삭제", + "name_raw": { + "ko": "쿠폰 삭제", + "en": "Delete Coupon" + }, + "description": "쿠폰 삭제", + "description_raw": { + "ko": "쿠폰 삭제", + "en": "Delete coupon" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "coupon", + "owner_key": "created_by", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 58, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.shipping-policies", + "name": "배송정책 관리", + "name_raw": { + "ko": "배송정책 관리", + "en": "Shipping Policy Management" + }, + "description": "배송정책 관리 권한", + "description_raw": { + "ko": "배송정책 관리 권한", + "en": "Shipping policy management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 9, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 59, + "parent_id": 58, + "identifier": "sirsoft-ecommerce.shipping-policies.read", + "name": "배송정책 조회", + "name_raw": { + "ko": "배송정책 조회", + "en": "Read Shipping Policies" + }, + "description": "배송정책 목록 및 상세 조회", + "description_raw": { + "ko": "배송정책 목록 및 상세 조회", + "en": "Read shipping policy list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "shippingPolicy", + "owner_key": "created_by", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 60, + "parent_id": 58, + "identifier": "sirsoft-ecommerce.shipping-policies.create", + "name": "배송정책 생성", + "name_raw": { + "ko": "배송정책 생성", + "en": "Create Shipping Policy" + }, + "description": "새 배송정책 등록", + "description_raw": { + "ko": "새 배송정책 등록", + "en": "Create new shipping policy" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 61, + "parent_id": 58, + "identifier": "sirsoft-ecommerce.shipping-policies.update", + "name": "배송정책 수정", + "name_raw": { + "ko": "배송정책 수정", + "en": "Update Shipping Policy" + }, + "description": "배송정책 정보 수정", + "description_raw": { + "ko": "배송정책 정보 수정", + "en": "Update shipping policy information" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "shippingPolicy", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 62, + "parent_id": 58, + "identifier": "sirsoft-ecommerce.shipping-policies.delete", + "name": "배송정책 삭제", + "name_raw": { + "ko": "배송정책 삭제", + "en": "Delete Shipping Policy" + }, + "description": "배송정책 삭제", + "description_raw": { + "ko": "배송정책 삭제", + "en": "Delete shipping policy" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "shippingPolicy", + "owner_key": "created_by", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 63, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.product-labels", + "name": "상품 라벨 관리", + "name_raw": { + "ko": "상품 라벨 관리", + "en": "Product Label Management" + }, + "description": "상품 라벨 관리 권한", + "description_raw": { + "ko": "상품 라벨 관리 권한", + "en": "Product label management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 10, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 64, + "parent_id": 63, + "identifier": "sirsoft-ecommerce.product-labels.read", + "name": "상품 라벨 조회", + "name_raw": { + "ko": "상품 라벨 조회", + "en": "Read Product Labels" + }, + "description": "상품 라벨 목록 및 상세 조회", + "description_raw": { + "ko": "상품 라벨 목록 및 상세 조회", + "en": "Read product label list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 65, + "parent_id": 63, + "identifier": "sirsoft-ecommerce.product-labels.create", + "name": "상품 라벨 생성", + "name_raw": { + "ko": "상품 라벨 생성", + "en": "Create Product Label" + }, + "description": "새 상품 라벨 등록", + "description_raw": { + "ko": "새 상품 라벨 등록", + "en": "Create new product label" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 66, + "parent_id": 63, + "identifier": "sirsoft-ecommerce.product-labels.update", + "name": "상품 라벨 수정", + "name_raw": { + "ko": "상품 라벨 수정", + "en": "Update Product Label" + }, + "description": "상품 라벨 정보 수정", + "description_raw": { + "ko": "상품 라벨 정보 수정", + "en": "Update product label information" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 67, + "parent_id": 63, + "identifier": "sirsoft-ecommerce.product-labels.delete", + "name": "상품 라벨 삭제", + "name_raw": { + "ko": "상품 라벨 삭제", + "en": "Delete Product Label" + }, + "description": "상품 라벨 삭제", + "description_raw": { + "ko": "상품 라벨 삭제", + "en": "Delete product label" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 68, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.identity.policies", + "name": "이커머스 본인인증 정책", + "name_raw": { + "ko": "이커머스 본인인증 정책", + "en": "Ecommerce Identity Policies" + }, + "description": "이커머스 컨텍스트의 본인인증 정책 관리 권한", + "description_raw": { + "ko": "이커머스 컨텍스트의 본인인증 정책 관리 권한", + "en": "Manage identity verification policies in ecommerce context" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 11, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 69, + "parent_id": 68, + "identifier": "sirsoft-ecommerce.identity.policies.read", + "name": "본인인증 정책 조회", + "name_raw": { + "ko": "본인인증 정책 조회", + "en": "View Identity Policies" + }, + "description": "이커머스 본인인증 정책 조회", + "description_raw": { + "ko": "이커머스 본인인증 정책 조회", + "en": "View ecommerce identity policies" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 70, + "parent_id": 68, + "identifier": "sirsoft-ecommerce.identity.policies.update", + "name": "본인인증 정책 수정", + "name_raw": { + "ko": "본인인증 정책 수정", + "en": "Update Identity Policies" + }, + "description": "이커머스 본인인증 정책 수정/추가/삭제", + "description_raw": { + "ko": "이커머스 본인인증 정책 수정/추가/삭제", + "en": "Update, add, delete ecommerce identity policies" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 71, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.reviews", + "name": "리뷰 관리", + "name_raw": { + "ko": "리뷰 관리", + "en": "Review Management" + }, + "description": "상품 리뷰 관리 권한", + "description_raw": { + "ko": "상품 리뷰 관리 권한", + "en": "Product review management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 12, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 72, + "parent_id": 71, + "identifier": "sirsoft-ecommerce.reviews.read", + "name": "리뷰 조회", + "name_raw": { + "ko": "리뷰 조회", + "en": "Read Reviews" + }, + "description": "리뷰 목록 및 상세 조회", + "description_raw": { + "ko": "리뷰 목록 및 상세 조회", + "en": "Read review list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "review", + "owner_key": "user_id", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 73, + "parent_id": 71, + "identifier": "sirsoft-ecommerce.reviews.update", + "name": "리뷰 처리", + "name_raw": { + "ko": "리뷰 처리", + "en": "Manage Review" + }, + "description": "리뷰 상태 변경, 답변 등록/수정/삭제, 일괄 처리", + "description_raw": { + "ko": "리뷰 상태 변경, 답변 등록/수정/삭제, 일괄 처리", + "en": "Update review status, manage replies, bulk actions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "review", + "owner_key": "user_id", + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 74, + "parent_id": 71, + "identifier": "sirsoft-ecommerce.reviews.delete", + "name": "리뷰 삭제", + "name_raw": { + "ko": "리뷰 삭제", + "en": "Delete Review" + }, + "description": "리뷰 삭제", + "description_raw": { + "ko": "리뷰 삭제", + "en": "Delete review" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "review", + "owner_key": "user_id", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 75, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.inquiries", + "name": "문의 관리", + "name_raw": { + "ko": "문의 관리", + "en": "Inquiry Management" + }, + "description": "상품 1:1 문의 관리 권한", + "description_raw": { + "ko": "상품 1:1 문의 관리 권한", + "en": "Product inquiry management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 13, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 76, + "parent_id": 75, + "identifier": "sirsoft-ecommerce.inquiries.update", + "name": "문의 처리", + "name_raw": { + "ko": "문의 처리", + "en": "Manage Inquiry" + }, + "description": "답변 등록/수정/삭제, 비밀글 내용 열람", + "description_raw": { + "ko": "답변 등록/수정/삭제, 비밀글 내용 열람", + "en": "Create, update, and delete inquiry replies; view secret inquiry contents" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 77, + "parent_id": 75, + "identifier": "sirsoft-ecommerce.inquiries.delete", + "name": "문의 삭제", + "name_raw": { + "ko": "문의 삭제", + "en": "Delete Inquiry" + }, + "description": "고객이 작성한 문의 자체를 삭제", + "description_raw": { + "ko": "고객이 작성한 문의 자체를 삭제", + "en": "Delete inquiries submitted by customers" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 78, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.dashboard", + "name": "대시보드", + "name_raw": { + "ko": "대시보드", + "en": "Dashboard" + }, + "description": "관리자 대시보드 이커머스 영역 조회 권한", + "description_raw": { + "ko": "관리자 대시보드 이커머스 영역 조회 권한", + "en": "Admin dashboard commerce area view permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 14, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 79, + "parent_id": 78, + "identifier": "sirsoft-ecommerce.dashboard.view", + "name": "대시보드 조회", + "name_raw": { + "ko": "대시보드 조회", + "en": "View Dashboard" + }, + "description": "관리자 대시보드의 이커머스 판매 현황/리뷰/문의 위젯 조회", + "description_raw": { + "ko": "관리자 대시보드의 이커머스 판매 현황/리뷰/문의 위젯 조회", + "en": "View commerce sales/review/inquiry widgets on the admin dashboard" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 80, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-products", + "name": "사용자 상품", + "name_raw": { + "ko": "사용자 상품", + "en": "User Products" + }, + "description": "사용자 상품 접근 권한 (블랙컨슈머 차단용)", + "description_raw": { + "ko": "사용자 상품 접근 권한 (블랙컨슈머 차단용)", + "en": "User product access permissions (for blocking malicious consumers)" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 15, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 81, + "parent_id": 80, + "identifier": "sirsoft-ecommerce.user-products.read", + "name": "상품 조회", + "name_raw": { + "ko": "상품 조회", + "en": "View Products" + }, + "description": "상품 목록 및 상세 조회", + "description_raw": { + "ko": "상품 목록 및 상세 조회", + "en": "View product list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 82, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-orders", + "name": "사용자 주문", + "name_raw": { + "ko": "사용자 주문", + "en": "User Orders" + }, + "description": "사용자 주문 관련 권한 (블랙컨슈머 차단용)", + "description_raw": { + "ko": "사용자 주문 관련 권한 (블랙컨슈머 차단용)", + "en": "User order permissions (for blocking malicious consumers)" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 16, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 83, + "parent_id": 82, + "identifier": "sirsoft-ecommerce.user-orders.create", + "name": "주문하기", + "name_raw": { + "ko": "주문하기", + "en": "Create Order" + }, + "description": "주문 생성", + "description_raw": { + "ko": "주문 생성", + "en": "Create a new order" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 84, + "parent_id": 82, + "identifier": "sirsoft-ecommerce.user-orders.cancel", + "name": "주문 취소", + "name_raw": { + "ko": "주문 취소", + "en": "Cancel Order" + }, + "description": "주문 취소 요청", + "description_raw": { + "ko": "주문 취소 요청", + "en": "Request order cancellation" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 85, + "parent_id": 82, + "identifier": "sirsoft-ecommerce.user-orders.confirm", + "name": "구매확정", + "name_raw": { + "ko": "구매확정", + "en": "Confirm Purchase" + }, + "description": "주문 상품 구매확정", + "description_raw": { + "ko": "주문 상품 구매확정", + "en": "Confirm purchase of order item" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 86, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-reviews", + "name": "사용자 리뷰", + "name_raw": { + "ko": "사용자 리뷰", + "en": "User Reviews" + }, + "description": "사용자 리뷰 관련 권한 (블랙컨슈머 차단용)", + "description_raw": { + "ko": "사용자 리뷰 관련 권한 (블랙컨슈머 차단용)", + "en": "User review permissions (for blocking malicious consumers)" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 17, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 87, + "parent_id": 86, + "identifier": "sirsoft-ecommerce.user-reviews.write", + "name": "리뷰 작성", + "name_raw": { + "ko": "리뷰 작성", + "en": "Write Review" + }, + "description": "상품 리뷰 작성", + "description_raw": { + "ko": "상품 리뷰 작성", + "en": "Write product review" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 88, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.mileage", + "name": "마일리지 관리", + "name_raw": { + "ko": "마일리지 관리", + "en": "Mileage Management" + }, + "description": "마일리지 내역 조회 및 수동 지급/차감 권한", + "description_raw": { + "ko": "마일리지 내역 조회 및 수동 지급/차감 권한", + "en": "Mileage history and manual grant/deduct permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 18, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 89, + "parent_id": 88, + "identifier": "sirsoft-ecommerce.mileage.read", + "name": "마일리지 내역 조회", + "name_raw": { + "ko": "마일리지 내역 조회", + "en": "Read Mileage" + }, + "description": "마일리지 내역 목록 및 상세 조회", + "description_raw": { + "ko": "마일리지 내역 목록 및 상세 조회", + "en": "Read mileage history list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "mileage-transaction", + "owner_key": "user_id", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 90, + "parent_id": 88, + "identifier": "sirsoft-ecommerce.mileage.manage", + "name": "마일리지 수동 처리", + "name_raw": { + "ko": "마일리지 수동 처리", + "en": "Manage Mileage" + }, + "description": "마일리지 수동 지급/차감 및 일괄 유효기간 연장", + "description_raw": { + "ko": "마일리지 수동 지급/차감 및 일괄 유효기간 연장", + "en": "Manual grant/deduct and bulk expiry extension" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": "mileage-transaction", + "owner_key": "user_id", + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 91, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-currency", + "name": "회원 결제 통화 관리", + "name_raw": { + "ko": "회원 결제 통화 관리", + "en": "User Currency Management" + }, + "description": "관리자가 회원별 결제 통화를 변경하는 권한", + "description_raw": { + "ko": "관리자가 회원별 결제 통화를 변경하는 권한", + "en": "Permission for admins to change a user's payment currency" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 19, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 92, + "parent_id": 91, + "identifier": "sirsoft-ecommerce.user-currency.manage", + "name": "회원 결제 통화 변경", + "name_raw": { + "ko": "회원 결제 통화 변경", + "en": "Manage User Currency" + }, + "description": "회원별 결제 통화 변경", + "description_raw": { + "ko": "회원별 결제 통화 변경", + "en": "Change a user's payment currency" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 93, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-shipping-country", + "name": "회원 배송국가 관리", + "name_raw": { + "ko": "회원 배송국가 관리", + "en": "User Shipping Country Management" + }, + "description": "관리자가 회원별 배송국가를 변경하는 권한", + "description_raw": { + "ko": "관리자가 회원별 배송국가를 변경하는 권한", + "en": "Permission for admins to change a user's shipping country" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 20, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 94, + "parent_id": 93, + "identifier": "sirsoft-ecommerce.user-shipping-country.manage", + "name": "회원 배송국가 변경", + "name_raw": { + "ko": "회원 배송국가 변경", + "en": "Manage User Shipping Country" + }, + "description": "회원별 배송국가 변경", + "description_raw": { + "ko": "회원별 배송국가 변경", + "en": "Change a user's shipping country" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + } + ] + }, + { + "id": 109, + "parent_id": null, + "identifier": "sirsoft-page", + "name": "페이지", + "name_raw": { + "ko": "페이지", + "en": "Page" + }, + "description": "페이지 모듈 권한", + "description_raw": { + "ko": "페이지 모듈 권한", + "en": "Page module permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "resource_route_key": null, + "owner_key": null, + "order": 100, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 110, + "parent_id": 109, + "identifier": "sirsoft-page.pages", + "name": "페이지 관리", + "name_raw": { + "ko": "페이지 관리", + "en": "Page Management" + }, + "description": "페이지 관리 권한", + "description_raw": { + "ko": "페이지 관리 권한", + "en": "Page management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 111, + "parent_id": 110, + "identifier": "sirsoft-page.pages.read", + "name": "페이지 조회", + "name_raw": { + "ko": "페이지 조회", + "en": "View Pages" + }, + "description": "페이지 목록 및 상세 조회", + "description_raw": { + "ko": "페이지 목록 및 상세 조회", + "en": "View page list and details" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "resource_route_key": "page", + "owner_key": "created_by", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 112, + "parent_id": 110, + "identifier": "sirsoft-page.pages.create", + "name": "페이지 생성", + "name_raw": { + "ko": "페이지 생성", + "en": "Create Page" + }, + "description": "새 페이지 생성", + "description_raw": { + "ko": "새 페이지 생성", + "en": "Create new page" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 113, + "parent_id": 110, + "identifier": "sirsoft-page.pages.update", + "name": "페이지 수정", + "name_raw": { + "ko": "페이지 수정", + "en": "Update Page" + }, + "description": "페이지 내용 수정", + "description_raw": { + "ko": "페이지 내용 수정", + "en": "Update page content" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "resource_route_key": "page", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 114, + "parent_id": 110, + "identifier": "sirsoft-page.pages.delete", + "name": "페이지 삭제", + "name_raw": { + "ko": "페이지 삭제", + "en": "Delete Page" + }, + "description": "페이지 삭제", + "description_raw": { + "ko": "페이지 삭제", + "en": "Delete page" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "resource_route_key": "page", + "owner_key": "created_by", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + } + ] + }, + { + "id": 115, + "parent_id": null, + "identifier": "core", + "name": "코어", + "name_raw": { + "ko": "코어", + "en": "Core" + }, + "description": "코어 시스템 권한", + "description_raw": { + "ko": "코어 시스템 권한", + "en": "Core system permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 116, + "parent_id": 115, + "identifier": "core.users", + "name": "사용자 관리", + "name_raw": { + "ko": "사용자 관리", + "en": "User Management" + }, + "description": "사용자 관리 권한", + "description_raw": { + "ko": "사용자 관리 권한", + "en": "User management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 117, + "parent_id": 116, + "identifier": "core.users.read", + "name": "사용자 조회", + "name_raw": { + "ko": "사용자 조회", + "en": "View Users" + }, + "description": "사용자 목록 및 상세 정보를 조회할 수 있습니다.", + "description_raw": { + "ko": "사용자 목록 및 상세 정보를 조회할 수 있습니다.", + "en": "Can view user list and details." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "user", + "owner_key": "id", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 118, + "parent_id": 116, + "identifier": "core.users.create", + "name": "사용자 생성", + "name_raw": { + "ko": "사용자 생성", + "en": "Create Users" + }, + "description": "새로운 사용자를 생성할 수 있습니다.", + "description_raw": { + "ko": "새로운 사용자를 생성할 수 있습니다.", + "en": "Can create new users." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 119, + "parent_id": 116, + "identifier": "core.users.update", + "name": "사용자 수정", + "name_raw": { + "ko": "사용자 수정", + "en": "Update Users" + }, + "description": "사용자 정보를 수정할 수 있습니다.", + "description_raw": { + "ko": "사용자 정보를 수정할 수 있습니다.", + "en": "Can update user information." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "user", + "owner_key": "id", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 120, + "parent_id": 116, + "identifier": "core.users.delete", + "name": "사용자 삭제", + "name_raw": { + "ko": "사용자 삭제", + "en": "Delete Users" + }, + "description": "사용자를 삭제할 수 있습니다.", + "description_raw": { + "ko": "사용자를 삭제할 수 있습니다.", + "en": "Can delete users." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "user", + "owner_key": "id", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 121, + "parent_id": 115, + "identifier": "core.menus", + "name": "메뉴 관리", + "name_raw": { + "ko": "메뉴 관리", + "en": "Menu Management" + }, + "description": "메뉴 관리 권한", + "description_raw": { + "ko": "메뉴 관리 권한", + "en": "Menu management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 122, + "parent_id": 121, + "identifier": "core.menus.read", + "name": "메뉴 조회", + "name_raw": { + "ko": "메뉴 조회", + "en": "View Menus" + }, + "description": "메뉴 목록을 조회할 수 있습니다.", + "description_raw": { + "ko": "메뉴 목록을 조회할 수 있습니다.", + "en": "Can view menu list." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "menu", + "owner_key": "created_by", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 123, + "parent_id": 121, + "identifier": "core.menus.create", + "name": "메뉴 생성", + "name_raw": { + "ko": "메뉴 생성", + "en": "Create Menus" + }, + "description": "새로운 메뉴를 생성할 수 있습니다.", + "description_raw": { + "ko": "새로운 메뉴를 생성할 수 있습니다.", + "en": "Can create new menus." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 124, + "parent_id": 121, + "identifier": "core.menus.update", + "name": "메뉴 수정", + "name_raw": { + "ko": "메뉴 수정", + "en": "Update Menus" + }, + "description": "메뉴 정보를 수정할 수 있습니다.", + "description_raw": { + "ko": "메뉴 정보를 수정할 수 있습니다.", + "en": "Can update menu information." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "menu", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 125, + "parent_id": 121, + "identifier": "core.menus.delete", + "name": "메뉴 삭제", + "name_raw": { + "ko": "메뉴 삭제", + "en": "Delete Menus" + }, + "description": "메뉴를 삭제할 수 있습니다.", + "description_raw": { + "ko": "메뉴를 삭제할 수 있습니다.", + "en": "Can delete menus." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "menu", + "owner_key": "created_by", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 126, + "parent_id": 115, + "identifier": "core.modules", + "name": "모듈 관리", + "name_raw": { + "ko": "모듈 관리", + "en": "Module Management" + }, + "description": "모듈 관리 권한", + "description_raw": { + "ko": "모듈 관리 권한", + "en": "Module management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 127, + "parent_id": 126, + "identifier": "core.modules.read", + "name": "모듈 조회", + "name_raw": { + "ko": "모듈 조회", + "en": "View Modules" + }, + "description": "모듈 목록을 조회할 수 있습니다.", + "description_raw": { + "ko": "모듈 목록을 조회할 수 있습니다.", + "en": "Can view module list." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 128, + "parent_id": 126, + "identifier": "core.modules.install", + "name": "모듈 설치", + "name_raw": { + "ko": "모듈 설치", + "en": "Install Modules" + }, + "description": "새로운 모듈을 설치할 수 있습니다.", + "description_raw": { + "ko": "새로운 모듈을 설치할 수 있습니다.", + "en": "Can install new modules." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 129, + "parent_id": 126, + "identifier": "core.modules.activate", + "name": "모듈 활성화", + "name_raw": { + "ko": "모듈 활성화", + "en": "Activate Modules" + }, + "description": "모듈을 활성화/비활성화할 수 있습니다.", + "description_raw": { + "ko": "모듈을 활성화/비활성화할 수 있습니다.", + "en": "Can activate/deactivate modules." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 130, + "parent_id": 126, + "identifier": "core.modules.uninstall", + "name": "모듈 삭제", + "name_raw": { + "ko": "모듈 삭제", + "en": "Uninstall Modules" + }, + "description": "모듈을 삭제할 수 있습니다.", + "description_raw": { + "ko": "모듈을 삭제할 수 있습니다.", + "en": "Can uninstall modules." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 131, + "parent_id": 115, + "identifier": "core.plugins", + "name": "플러그인 관리", + "name_raw": { + "ko": "플러그인 관리", + "en": "Plugin Management" + }, + "description": "플러그인 관리 권한", + "description_raw": { + "ko": "플러그인 관리 권한", + "en": "Plugin management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 132, + "parent_id": 131, + "identifier": "core.plugins.read", + "name": "플러그인 조회", + "name_raw": { + "ko": "플러그인 조회", + "en": "View Plugins" + }, + "description": "플러그인 목록을 조회할 수 있습니다.", + "description_raw": { + "ko": "플러그인 목록을 조회할 수 있습니다.", + "en": "Can view plugin list." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 133, + "parent_id": 131, + "identifier": "core.plugins.install", + "name": "플러그인 설치", + "name_raw": { + "ko": "플러그인 설치", + "en": "Install Plugins" + }, + "description": "새로운 플러그인을 설치할 수 있습니다.", + "description_raw": { + "ko": "새로운 플러그인을 설치할 수 있습니다.", + "en": "Can install new plugins." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 134, + "parent_id": 131, + "identifier": "core.plugins.activate", + "name": "플러그인 활성화", + "name_raw": { + "ko": "플러그인 활성화", + "en": "Activate Plugins" + }, + "description": "플러그인을 활성화/비활성화할 수 있습니다.", + "description_raw": { + "ko": "플러그인을 활성화/비활성화할 수 있습니다.", + "en": "Can activate/deactivate plugins." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 135, + "parent_id": 131, + "identifier": "core.plugins.update", + "name": "플러그인 설정", + "name_raw": { + "ko": "플러그인 설정", + "en": "Configure Plugins" + }, + "description": "플러그인 환경설정을 수정할 수 있습니다.", + "description_raw": { + "ko": "플러그인 환경설정을 수정할 수 있습니다.", + "en": "Can update plugin settings." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 136, + "parent_id": 131, + "identifier": "core.plugins.uninstall", + "name": "플러그인 삭제", + "name_raw": { + "ko": "플러그인 삭제", + "en": "Uninstall Plugins" + }, + "description": "플러그인을 삭제할 수 있습니다.", + "description_raw": { + "ko": "플러그인을 삭제할 수 있습니다.", + "en": "Can uninstall plugins." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 5, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 137, + "parent_id": 115, + "identifier": "core.templates", + "name": "템플릿 관리", + "name_raw": { + "ko": "템플릿 관리", + "en": "Template Management" + }, + "description": "템플릿 관리 권한", + "description_raw": { + "ko": "템플릿 관리 권한", + "en": "Template management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 5, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 138, + "parent_id": 137, + "identifier": "core.templates.read", + "name": "템플릿 조회", + "name_raw": { + "ko": "템플릿 조회", + "en": "View Templates" + }, + "description": "템플릿 목록을 조회할 수 있습니다.", + "description_raw": { + "ko": "템플릿 목록을 조회할 수 있습니다.", + "en": "Can view template list." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 139, + "parent_id": 137, + "identifier": "core.templates.install", + "name": "템플릿 설치", + "name_raw": { + "ko": "템플릿 설치", + "en": "Install Templates" + }, + "description": "새로운 템플릿을 설치할 수 있습니다.", + "description_raw": { + "ko": "새로운 템플릿을 설치할 수 있습니다.", + "en": "Can install new templates." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 140, + "parent_id": 137, + "identifier": "core.templates.activate", + "name": "템플릿 활성화", + "name_raw": { + "ko": "템플릿 활성화", + "en": "Activate Templates" + }, + "description": "템플릿을 활성화/비활성화할 수 있습니다.", + "description_raw": { + "ko": "템플릿을 활성화/비활성화할 수 있습니다.", + "en": "Can activate/deactivate templates." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 141, + "parent_id": 137, + "identifier": "core.templates.uninstall", + "name": "템플릿 삭제", + "name_raw": { + "ko": "템플릿 삭제", + "en": "Uninstall Templates" + }, + "description": "템플릿을 삭제할 수 있습니다.", + "description_raw": { + "ko": "템플릿을 삭제할 수 있습니다.", + "en": "Can uninstall templates." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 142, + "parent_id": 137, + "identifier": "core.templates.layouts.edit", + "name": "레이아웃 편집", + "name_raw": { + "ko": "레이아웃 편집", + "en": "Edit Layouts" + }, + "description": "템플릿 레이아웃을 편집할 수 있습니다.", + "description_raw": { + "ko": "템플릿 레이아웃을 편집할 수 있습니다.", + "en": "Can edit template layouts." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 5, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 143, + "parent_id": 115, + "identifier": "core.permissions", + "name": "권한 관리", + "name_raw": { + "ko": "권한 관리", + "en": "Permission Management" + }, + "description": "역할 및 권한 관리 권한", + "description_raw": { + "ko": "역할 및 권한 관리 권한", + "en": "Role and permission management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 6, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 144, + "parent_id": 143, + "identifier": "core.permissions.read", + "name": "권한 조회", + "name_raw": { + "ko": "권한 조회", + "en": "View Permissions" + }, + "description": "역할 및 권한 목록을 조회할 수 있습니다.", + "description_raw": { + "ko": "역할 및 권한 목록을 조회할 수 있습니다.", + "en": "Can view roles and permissions." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 145, + "parent_id": 143, + "identifier": "core.permissions.create", + "name": "역할 생성", + "name_raw": { + "ko": "역할 생성", + "en": "Create Roles" + }, + "description": "새로운 역할을 생성할 수 있습니다.", + "description_raw": { + "ko": "새로운 역할을 생성할 수 있습니다.", + "en": "Can create new roles." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 146, + "parent_id": 143, + "identifier": "core.permissions.update", + "name": "역할 수정", + "name_raw": { + "ko": "역할 수정", + "en": "Update Roles" + }, + "description": "역할 정보와 권한을 수정할 수 있습니다.", + "description_raw": { + "ko": "역할 정보와 권한을 수정할 수 있습니다.", + "en": "Can update role information and permissions." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 147, + "parent_id": 143, + "identifier": "core.permissions.delete", + "name": "역할 삭제", + "name_raw": { + "ko": "역할 삭제", + "en": "Delete Roles" + }, + "description": "역할을 삭제할 수 있습니다.", + "description_raw": { + "ko": "역할을 삭제할 수 있습니다.", + "en": "Can delete roles." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 148, + "parent_id": 115, + "identifier": "core.notification-logs", + "name": "알림 발송 이력", + "name_raw": { + "ko": "알림 발송 이력", + "en": "Notification Logs" + }, + "description": "알림 발송 이력 관리 권한", + "description_raw": { + "ko": "알림 발송 이력 관리 권한", + "en": "Notification log management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 7, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 149, + "parent_id": 148, + "identifier": "core.notification-logs.read", + "name": "발송 이력 조회", + "name_raw": { + "ko": "발송 이력 조회", + "en": "View Notification Logs" + }, + "description": "알림 발송 이력을 조회할 수 있습니다.", + "description_raw": { + "ko": "알림 발송 이력을 조회할 수 있습니다.", + "en": "Can view notification logs." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 150, + "parent_id": 148, + "identifier": "core.notification-logs.delete", + "name": "발송 이력 삭제", + "name_raw": { + "ko": "발송 이력 삭제", + "en": "Delete Notification Logs" + }, + "description": "알림 발송 이력을 삭제할 수 있습니다.", + "description_raw": { + "ko": "알림 발송 이력을 삭제할 수 있습니다.", + "en": "Can delete notification logs." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 151, + "parent_id": 115, + "identifier": "core.notifications", + "name": "알림 (관리자)", + "name_raw": { + "ko": "알림 (관리자)", + "en": "Notifications (Admin)" + }, + "description": "관리자용 알림 관리 권한 (관리자 화면에서 사용)", + "description_raw": { + "ko": "관리자용 알림 관리 권한 (관리자 화면에서 사용)", + "en": "Admin notification management permissions (used in admin UI)" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 152, + "parent_id": 151, + "identifier": "core.notifications.read", + "name": "알림 조회", + "name_raw": { + "ko": "알림 조회", + "en": "View Notifications" + }, + "description": "알림 목록 및 읽지 않은 수를 조회할 수 있습니다.", + "description_raw": { + "ko": "알림 목록 및 읽지 않은 수를 조회할 수 있습니다.", + "en": "Can view notification list and unread count." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 153, + "parent_id": 151, + "identifier": "core.notifications.update", + "name": "알림 읽음 처리", + "name_raw": { + "ko": "알림 읽음 처리", + "en": "Mark Notifications Read" + }, + "description": "알림을 읽음 처리할 수 있습니다.", + "description_raw": { + "ko": "알림을 읽음 처리할 수 있습니다.", + "en": "Can mark notifications as read." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 154, + "parent_id": 151, + "identifier": "core.notifications.delete", + "name": "알림 삭제", + "name_raw": { + "ko": "알림 삭제", + "en": "Delete Notifications" + }, + "description": "알림을 삭제할 수 있습니다.", + "description_raw": { + "ko": "알림을 삭제할 수 있습니다.", + "en": "Can delete notifications." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 155, + "parent_id": 115, + "identifier": "core.user-notifications", + "name": "알림 (사용자)", + "name_raw": { + "ko": "알림 (사용자)", + "en": "Notifications (User)" + }, + "description": "사용자용 알림 권한 (사용자 화면에서 본인 알림 관리)", + "description_raw": { + "ko": "사용자용 알림 권한 (사용자 화면에서 본인 알림 관리)", + "en": "User notification permissions (managing own notifications in user UI)" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 156, + "parent_id": 155, + "identifier": "core.user-notifications.read", + "name": "알림 조회", + "name_raw": { + "ko": "알림 조회", + "en": "View Notifications" + }, + "description": "본인의 알림 목록 및 읽지 않은 수를 조회할 수 있습니다.", + "description_raw": { + "ko": "본인의 알림 목록 및 읽지 않은 수를 조회할 수 있습니다.", + "en": "Can view own notification list and unread count." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 157, + "parent_id": 155, + "identifier": "core.user-notifications.update", + "name": "알림 읽음 처리", + "name_raw": { + "ko": "알림 읽음 처리", + "en": "Mark Notifications Read" + }, + "description": "본인의 알림을 읽음 처리할 수 있습니다.", + "description_raw": { + "ko": "본인의 알림을 읽음 처리할 수 있습니다.", + "en": "Can mark own notifications as read." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 158, + "parent_id": 155, + "identifier": "core.user-notifications.delete", + "name": "알림 삭제", + "name_raw": { + "ko": "알림 삭제", + "en": "Delete Notifications" + }, + "description": "본인의 알림을 삭제할 수 있습니다.", + "description_raw": { + "ko": "본인의 알림을 삭제할 수 있습니다.", + "en": "Can delete own notifications." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 159, + "parent_id": 115, + "identifier": "core.identity", + "name": "본인인증 (사용자)", + "name_raw": { + "ko": "본인인증 (사용자)", + "en": "Identity Verification (User)" + }, + "description": "로그인 사용자의 본인인증 challenge 요청/검증/취소 권한", + "description_raw": { + "ko": "로그인 사용자의 본인인증 challenge 요청/검증/취소 권한", + "en": "Permissions for authenticated users to request/verify/cancel IDV challenges" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 160, + "parent_id": 159, + "identifier": "core.identity.request", + "name": "IDV 요청", + "name_raw": { + "ko": "IDV 요청", + "en": "Request IDV Challenge" + }, + "description": "본인인증 challenge 를 요청할 수 있습니다.", + "description_raw": { + "ko": "본인인증 challenge 를 요청할 수 있습니다.", + "en": "Can request identity verification challenges." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 161, + "parent_id": 159, + "identifier": "core.identity.verify", + "name": "IDV 검증", + "name_raw": { + "ko": "IDV 검증", + "en": "Verify IDV Challenge" + }, + "description": "본인의 본인인증 challenge 를 검증할 수 있습니다.", + "description_raw": { + "ko": "본인의 본인인증 challenge 를 검증할 수 있습니다.", + "en": "Can verify own identity verification challenges." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "challenge", + "owner_key": "user_id", + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 162, + "parent_id": 159, + "identifier": "core.identity.cancel", + "name": "IDV 취소", + "name_raw": { + "ko": "IDV 취소", + "en": "Cancel IDV Challenge" + }, + "description": "본인의 본인인증 challenge 를 취소할 수 있습니다.", + "description_raw": { + "ko": "본인의 본인인증 challenge 를 취소할 수 있습니다.", + "en": "Can cancel own identity verification challenges." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "challenge", + "owner_key": "user_id", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 163, + "parent_id": 115, + "identifier": "core.admin.identity", + "name": "본인인증 관리 (관리자)", + "name_raw": { + "ko": "본인인증 관리 (관리자)", + "en": "Identity Verification Management (Admin)" + }, + "description": "관리자용 IDV 프로바이더/정책/로그 관리 권한", + "description_raw": { + "ko": "관리자용 IDV 프로바이더/정책/로그 관리 권한", + "en": "Admin permissions for IDV providers, policies, and logs" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 164, + "parent_id": 163, + "identifier": "core.admin.identity.providers.read", + "name": "프로바이더 설정 조회", + "name_raw": { + "ko": "프로바이더 설정 조회", + "en": "View IDV Providers" + }, + "description": "본인인증 프로바이더 설정을 조회할 수 있습니다.", + "description_raw": { + "ko": "본인인증 프로바이더 설정을 조회할 수 있습니다.", + "en": "Can view identity verification providers." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 165, + "parent_id": 163, + "identifier": "core.admin.identity.providers.update", + "name": "프로바이더 설정 수정", + "name_raw": { + "ko": "프로바이더 설정 수정", + "en": "Update IDV Providers" + }, + "description": "본인인증 프로바이더 설정을 수정할 수 있습니다.", + "description_raw": { + "ko": "본인인증 프로바이더 설정을 수정할 수 있습니다.", + "en": "Can update identity verification providers." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 166, + "parent_id": 163, + "identifier": "core.admin.identity.policies.read", + "name": "정책 조회", + "name_raw": { + "ko": "정책 조회", + "en": "View IDV Policies" + }, + "description": "본인인증 정책(라우트/훅별)을 조회할 수 있습니다.", + "description_raw": { + "ko": "본인인증 정책(라우트/훅별)을 조회할 수 있습니다.", + "en": "Can view identity verification policies by route/hook." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 167, + "parent_id": 163, + "identifier": "core.admin.identity.policies.update", + "name": "정책 수정", + "name_raw": { + "ko": "정책 수정", + "en": "Update IDV Policies" + }, + "description": "본인인증 정책(라우트/훅별)을 CRUD 할 수 있습니다.", + "description_raw": { + "ko": "본인인증 정책(라우트/훅별)을 CRUD 할 수 있습니다.", + "en": "Can CRUD identity verification policies by route/hook." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 168, + "parent_id": 163, + "identifier": "core.admin.identity.logs.read", + "name": "로그 열람", + "name_raw": { + "ko": "로그 열람", + "en": "View IDV Logs" + }, + "description": "본인인증 이력을 조회할 수 있습니다.", + "description_raw": { + "ko": "본인인증 이력을 조회할 수 있습니다.", + "en": "Can view identity verification logs." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 5, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 169, + "parent_id": 163, + "identifier": "core.admin.identity.logs.purge", + "name": "로그 파기", + "name_raw": { + "ko": "로그 파기", + "en": "Purge IDV Logs" + }, + "description": "본인인증 이력을 파기(보관주기 외)할 수 있습니다.", + "description_raw": { + "ko": "본인인증 이력을 파기(보관주기 외)할 수 있습니다.", + "en": "Can purge identity verification logs (retention-based)." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 6, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 170, + "parent_id": 163, + "identifier": "core.admin.identity.messages.read", + "name": "메시지 템플릿 조회", + "name_raw": { + "ko": "메시지 템플릿 조회", + "en": "View IDV Message Templates" + }, + "description": "본인인증 메시지 정의/템플릿을 조회할 수 있습니다.", + "description_raw": { + "ko": "본인인증 메시지 정의/템플릿을 조회할 수 있습니다.", + "en": "Can view identity verification message definitions and templates." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 7, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 171, + "parent_id": 163, + "identifier": "core.admin.identity.messages.update", + "name": "메시지 템플릿 수정", + "name_raw": { + "ko": "메시지 템플릿 수정", + "en": "Update IDV Message Templates" + }, + "description": "본인인증 메시지 정의/템플릿을 수정할 수 있습니다.", + "description_raw": { + "ko": "본인인증 메시지 정의/템플릿을 수정할 수 있습니다.", + "en": "Can update identity verification message definitions and templates." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 172, + "parent_id": 115, + "identifier": "core.settings", + "name": "환경설정", + "name_raw": { + "ko": "환경설정", + "en": "Settings" + }, + "description": "시스템 환경설정 권한", + "description_raw": { + "ko": "시스템 환경설정 권한", + "en": "System settings permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 173, + "parent_id": 172, + "identifier": "core.settings.read", + "name": "설정 조회", + "name_raw": { + "ko": "설정 조회", + "en": "View Settings" + }, + "description": "시스템 설정을 조회할 수 있습니다.", + "description_raw": { + "ko": "시스템 설정을 조회할 수 있습니다.", + "en": "Can view system settings." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 174, + "parent_id": 172, + "identifier": "core.settings.update", + "name": "설정 수정", + "name_raw": { + "ko": "설정 수정", + "en": "Update Settings" + }, + "description": "시스템 설정을 수정할 수 있습니다.", + "description_raw": { + "ko": "시스템 설정을 수정할 수 있습니다.", + "en": "Can update system settings." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 175, + "parent_id": 115, + "identifier": "core.dashboard", + "name": "대시보드", + "name_raw": { + "ko": "대시보드", + "en": "Dashboard" + }, + "description": "대시보드 접근 권한", + "description_raw": { + "ko": "대시보드 접근 권한", + "en": "Dashboard access permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 9, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 176, + "parent_id": 175, + "identifier": "core.dashboard.read", + "name": "대시보드 조회", + "name_raw": { + "ko": "대시보드 조회", + "en": "View Dashboard" + }, + "description": "대시보드 통계 및 정보를 조회할 수 있습니다.", + "description_raw": { + "ko": "대시보드 통계 및 정보를 조회할 수 있습니다.", + "en": "Can view dashboard statistics and information." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 177, + "parent_id": 175, + "identifier": "core.dashboard.system-status", + "name": "시스템 상태", + "name_raw": { + "ko": "시스템 상태", + "en": "System Status" + }, + "description": "시스템 상태 정보를 조회할 수 있습니다.", + "description_raw": { + "ko": "시스템 상태 정보를 조회할 수 있습니다.", + "en": "Can view system status information." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 178, + "parent_id": 175, + "identifier": "core.dashboard.resources", + "name": "시스템 리소스", + "name_raw": { + "ko": "시스템 리소스", + "en": "System Resources" + }, + "description": "CPU, 메모리, 디스크 사용량을 조회할 수 있습니다.", + "description_raw": { + "ko": "CPU, 메모리, 디스크 사용량을 조회할 수 있습니다.", + "en": "Can view CPU, memory, and disk usage." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 179, + "parent_id": 175, + "identifier": "core.dashboard.activities", + "name": "최근 활동", + "name_raw": { + "ko": "최근 활동", + "en": "Recent Activities" + }, + "description": "최근 활동 이력을 조회할 수 있습니다.", + "description_raw": { + "ko": "최근 활동 이력을 조회할 수 있습니다.", + "en": "Can view recent activity history." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "activityLog", + "owner_key": "user_id", + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 180, + "parent_id": 175, + "identifier": "core.dashboard.alerts", + "name": "시스템 알림", + "name_raw": { + "ko": "시스템 알림", + "en": "System Alerts" + }, + "description": "시스템 알림을 조회할 수 있습니다.", + "description_raw": { + "ko": "시스템 알림을 조회할 수 있습니다.", + "en": "Can view system alerts." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 5, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 181, + "parent_id": 115, + "identifier": "core.activities", + "name": "활동 로그", + "name_raw": { + "ko": "활동 로그", + "en": "Activity Logs" + }, + "description": "활동 로그 조회 권한", + "description_raw": { + "ko": "활동 로그 조회 권한", + "en": "Activity log access permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 10, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 182, + "parent_id": 181, + "identifier": "core.activities.read", + "name": "활동 로그 조회", + "name_raw": { + "ko": "활동 로그 조회", + "en": "View Activity Logs" + }, + "description": "활동 로그를 조회할 수 있습니다.", + "description_raw": { + "ko": "활동 로그를 조회할 수 있습니다.", + "en": "Can view activity logs." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "activityLog", + "owner_key": "user_id", + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 183, + "parent_id": 181, + "identifier": "core.activities.delete", + "name": "활동 로그 삭제", + "name_raw": { + "ko": "활동 로그 삭제", + "en": "Delete Activity Logs" + }, + "description": "활동 로그를 삭제할 수 있습니다.", + "description_raw": { + "ko": "활동 로그를 삭제할 수 있습니다.", + "en": "Can delete activity logs." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 184, + "parent_id": 115, + "identifier": "core.attachments", + "name": "첨부파일 관리", + "name_raw": { + "ko": "첨부파일 관리", + "en": "Attachment Management" + }, + "description": "첨부파일 관리 권한", + "description_raw": { + "ko": "첨부파일 관리 권한", + "en": "Attachment management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 11, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 185, + "parent_id": 184, + "identifier": "core.attachments.create", + "name": "첨부파일 업로드", + "name_raw": { + "ko": "첨부파일 업로드", + "en": "Upload Attachments" + }, + "description": "첨부파일을 업로드할 수 있습니다.", + "description_raw": { + "ko": "첨부파일을 업로드할 수 있습니다.", + "en": "Can upload attachments." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 186, + "parent_id": 184, + "identifier": "core.attachments.update", + "name": "첨부파일 수정", + "name_raw": { + "ko": "첨부파일 수정", + "en": "Update Attachments" + }, + "description": "첨부파일 정보 및 순서를 수정할 수 있습니다.", + "description_raw": { + "ko": "첨부파일 정보 및 순서를 수정할 수 있습니다.", + "en": "Can update attachment information and order." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "attachment", + "owner_key": "created_by", + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 187, + "parent_id": 184, + "identifier": "core.attachments.delete", + "name": "첨부파일 삭제", + "name_raw": { + "ko": "첨부파일 삭제", + "en": "Delete Attachments" + }, + "description": "첨부파일을 삭제할 수 있습니다.", + "description_raw": { + "ko": "첨부파일을 삭제할 수 있습니다.", + "en": "Can delete attachments." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": "attachment", + "owner_key": "created_by", + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 188, + "parent_id": 115, + "identifier": "core.schedules", + "name": "스케줄 관리", + "name_raw": { + "ko": "스케줄 관리", + "en": "Schedule Management" + }, + "description": "스케줄 작업 관리 권한", + "description_raw": { + "ko": "스케줄 작업 관리 권한", + "en": "Schedule task management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 12, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 189, + "parent_id": 188, + "identifier": "core.schedules.read", + "name": "스케줄 조회", + "name_raw": { + "ko": "스케줄 조회", + "en": "View Schedules" + }, + "description": "스케줄 목록 및 상세 정보를 조회할 수 있습니다.", + "description_raw": { + "ko": "스케줄 목록 및 상세 정보를 조회할 수 있습니다.", + "en": "Can view schedule list and details." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 190, + "parent_id": 188, + "identifier": "core.schedules.create", + "name": "스케줄 생성", + "name_raw": { + "ko": "스케줄 생성", + "en": "Create Schedules" + }, + "description": "새로운 스케줄을 생성할 수 있습니다.", + "description_raw": { + "ko": "새로운 스케줄을 생성할 수 있습니다.", + "en": "Can create new schedules." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 191, + "parent_id": 188, + "identifier": "core.schedules.update", + "name": "스케줄 수정", + "name_raw": { + "ko": "스케줄 수정", + "en": "Update Schedules" + }, + "description": "스케줄 정보를 수정할 수 있습니다.", + "description_raw": { + "ko": "스케줄 정보를 수정할 수 있습니다.", + "en": "Can update schedule information." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 192, + "parent_id": 188, + "identifier": "core.schedules.delete", + "name": "스케줄 삭제", + "name_raw": { + "ko": "스케줄 삭제", + "en": "Delete Schedules" + }, + "description": "스케줄을 삭제할 수 있습니다.", + "description_raw": { + "ko": "스케줄을 삭제할 수 있습니다.", + "en": "Can delete schedules." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 193, + "parent_id": 188, + "identifier": "core.schedules.run", + "name": "스케줄 실행", + "name_raw": { + "ko": "스케줄 실행", + "en": "Run Schedules" + }, + "description": "스케줄을 수동으로 실행할 수 있습니다.", + "description_raw": { + "ko": "스케줄을 수동으로 실행할 수 있습니다.", + "en": "Can manually run schedules." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 5, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 194, + "parent_id": 115, + "identifier": "core.language_packs", + "name": "언어팩 관리", + "name_raw": { + "ko": "언어팩 관리", + "en": "Language Pack Management" + }, + "description": "언어팩 설치/제거/활성화 권한", + "description_raw": { + "ko": "언어팩 설치/제거/활성화 권한", + "en": "Language pack install/uninstall/activation permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 13, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 195, + "parent_id": 194, + "identifier": "core.language_packs.read", + "name": "언어팩 조회", + "name_raw": { + "ko": "언어팩 조회", + "en": "View Language Packs" + }, + "description": "설치된 언어팩 목록 및 상세 정보를 조회할 수 있습니다.", + "description_raw": { + "ko": "설치된 언어팩 목록 및 상세 정보를 조회할 수 있습니다.", + "en": "Can view installed language pack list and details." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 196, + "parent_id": 194, + "identifier": "core.language_packs.install", + "name": "언어팩 설치", + "name_raw": { + "ko": "언어팩 설치", + "en": "Install Language Packs" + }, + "description": "ZIP/GitHub/URL 로 언어팩을 설치할 수 있습니다.", + "description_raw": { + "ko": "ZIP/GitHub/URL 로 언어팩을 설치할 수 있습니다.", + "en": "Can install language packs from ZIP/GitHub/URL." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 197, + "parent_id": 194, + "identifier": "core.language_packs.manage", + "name": "언어팩 관리", + "name_raw": { + "ko": "언어팩 관리", + "en": "Manage Language Packs" + }, + "description": "언어팩을 활성화/비활성화/제거할 수 있습니다.", + "description_raw": { + "ko": "언어팩을 활성화/비활성화/제거할 수 있습니다.", + "en": "Can activate/deactivate/uninstall language packs." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + }, + { + "id": 198, + "parent_id": 194, + "identifier": "core.language_packs.update", + "name": "언어팩 업데이트", + "name_raw": { + "ko": "언어팩 업데이트", + "en": "Update Language Packs" + }, + "description": "설치된 언어팩의 업데이트를 확인하고 실행할 수 있습니다.", + "description_raw": { + "ko": "설치된 언어팩의 업데이트를 확인하고 실행할 수 있습니다.", + "en": "Can check and apply updates for installed language packs." + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + } + ] + }, + { + "id": 1, + "parent_id": null, + "identifier": "apidoc-sample.parent", + "name": "API 문서 샘플 권한", + "name_raw": { + "ko": "API 문서 샘플 권한", + "en": "API Doc Sample Permission" + }, + "description": "문서 실측용 권한", + "description_raw": { + "ko": "문서 실측용 권한", + "en": "Sample permission" + }, + "extension_type": null, + "extension_identifier": null, + "resource_route_key": null, + "owner_key": null, + "order": 0, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 2, + "parent_id": 1, + "identifier": "apidoc-sample.child", + "name": "하위 권한", + "name_raw": { + "ko": "하위 권한", + "en": "Child Permission" + }, + "description": "Quod consequatur sunt necessitatibus quam quia voluptas quod aperiam.", + "description_raw": { + "ko": "Quod consequatur sunt necessitatibus quam quia voluptas quod aperiam.", + "en": "Mollitia incidunt dolor rem maxime." + }, + "extension_type": null, + "extension_identifier": null, + "resource_route_key": null, + "owner_key": null, + "order": 0, + "is_assigned": true, + "is_assignable": true, + "scope_type": null + } + ] + }, + { + "id": 3, + "parent_id": null, + "identifier": "sirsoft-board", + "name": "게시판", + "name_raw": { + "ko": "게시판", + "en": "Board" + }, + "description": "게시판 모듈 권한", + "description_raw": { + "ko": "게시판 모듈 권한", + "en": "Board module permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 100, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 4, + "parent_id": 3, + "identifier": "sirsoft-board.apidoc-sample-board", + "name": "API 문서 샘플 게시판 게시판", + "name_raw": { + "ko": "API 문서 샘플 게시판 게시판", + "en": "API Doc Sample Board board" + }, + "description": "API 문서 샘플 게시판 게시판 권한", + "description_raw": { + "ko": "API 문서 샘플 게시판 게시판 권한", + "en": "API Doc Sample Board board permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 0, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 95, + "parent_id": 3, + "identifier": "sirsoft-board.boards", + "name": "게시판 관리", + "name_raw": { + "ko": "게시판 관리", + "en": "Board Management" + }, + "description": "게시판 관리 권한", + "description_raw": { + "ko": "게시판 관리 권한", + "en": "Board management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 100, + "parent_id": 3, + "identifier": "sirsoft-board.settings", + "name": "환경설정", + "name_raw": { + "ko": "환경설정", + "en": "Settings" + }, + "description": "게시판 환경설정 권한", + "description_raw": { + "ko": "게시판 환경설정 권한", + "en": "Board settings permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 103, + "parent_id": 3, + "identifier": "sirsoft-board.identity.policies", + "name": "게시판 본인인증 정책", + "name_raw": { + "ko": "게시판 본인인증 정책", + "en": "Board Identity Policies" + }, + "description": "게시판 컨텍스트의 본인인증 정책 관리 권한", + "description_raw": { + "ko": "게시판 컨텍스트의 본인인증 정책 관리 권한", + "en": "Manage identity verification policies in board context" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 106, + "parent_id": 3, + "identifier": "sirsoft-board.reports", + "name": "게시판 신고 관리", + "name_raw": { + "ko": "게시판 신고 관리", + "en": "Report Management" + }, + "description": "게시판 신고 관리 권한", + "description_raw": { + "ko": "게시판 신고 관리 권한", + "en": "Board report management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + } + ] + }, + { + "id": 21, + "parent_id": null, + "identifier": "sirsoft-ecommerce", + "name": "이커머스", + "name_raw": { + "ko": "이커머스", + "en": "Ecommerce" + }, + "description": "이커머스 모듈 권한", + "description_raw": { + "ko": "이커머스 모듈 권한", + "en": "Ecommerce module permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 100, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 22, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.products", + "name": "상품 관리", + "name_raw": { + "ko": "상품 관리", + "en": "Product Management" + }, + "description": "상품 관리 권한", + "description_raw": { + "ko": "상품 관리 권한", + "en": "Product management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 27, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.orders", + "name": "주문 관리", + "name_raw": { + "ko": "주문 관리", + "en": "Order Management" + }, + "description": "주문 관리 권한", + "description_raw": { + "ko": "주문 관리 권한", + "en": "Order management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 30, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.categories", + "name": "카테고리 관리", + "name_raw": { + "ko": "카테고리 관리", + "en": "Category Management" + }, + "description": "카테고리 관리 권한", + "description_raw": { + "ko": "카테고리 관리 권한", + "en": "Category management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 35, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.brands", + "name": "브랜드 관리", + "name_raw": { + "ko": "브랜드 관리", + "en": "Brand Management" + }, + "description": "브랜드 관리 권한", + "description_raw": { + "ko": "브랜드 관리 권한", + "en": "Brand management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 40, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.product-notice-templates", + "name": "상품정보제공고시 관리", + "name_raw": { + "ko": "상품정보제공고시 관리", + "en": "Product Notice Template Management" + }, + "description": "상품정보제공고시 템플릿 관리 권한", + "description_raw": { + "ko": "상품정보제공고시 템플릿 관리 권한", + "en": "Product notice template management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 5, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 45, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.product-common-infos", + "name": "공통정보 관리", + "name_raw": { + "ko": "공통정보 관리", + "en": "Product Common Info Management" + }, + "description": "상품 공통정보 관리 권한", + "description_raw": { + "ko": "상품 공통정보 관리 권한", + "en": "Product common info management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 6, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 50, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.settings", + "name": "환경설정", + "name_raw": { + "ko": "환경설정", + "en": "Settings" + }, + "description": "이커머스 환경설정 권한", + "description_raw": { + "ko": "이커머스 환경설정 권한", + "en": "Ecommerce settings permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 7, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 53, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.promotion-coupon", + "name": "쿠폰 관리", + "name_raw": { + "ko": "쿠폰 관리", + "en": "Coupon Management" + }, + "description": "쿠폰 관리 권한", + "description_raw": { + "ko": "쿠폰 관리 권한", + "en": "Coupon management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 58, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.shipping-policies", + "name": "배송정책 관리", + "name_raw": { + "ko": "배송정책 관리", + "en": "Shipping Policy Management" + }, + "description": "배송정책 관리 권한", + "description_raw": { + "ko": "배송정책 관리 권한", + "en": "Shipping policy management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 9, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 63, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.product-labels", + "name": "상품 라벨 관리", + "name_raw": { + "ko": "상품 라벨 관리", + "en": "Product Label Management" + }, + "description": "상품 라벨 관리 권한", + "description_raw": { + "ko": "상품 라벨 관리 권한", + "en": "Product label management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 10, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 68, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.identity.policies", + "name": "이커머스 본인인증 정책", + "name_raw": { + "ko": "이커머스 본인인증 정책", + "en": "Ecommerce Identity Policies" + }, + "description": "이커머스 컨텍스트의 본인인증 정책 관리 권한", + "description_raw": { + "ko": "이커머스 컨텍스트의 본인인증 정책 관리 권한", + "en": "Manage identity verification policies in ecommerce context" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 11, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 71, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.reviews", + "name": "리뷰 관리", + "name_raw": { + "ko": "리뷰 관리", + "en": "Review Management" + }, + "description": "상품 리뷰 관리 권한", + "description_raw": { + "ko": "상품 리뷰 관리 권한", + "en": "Product review management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 12, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 75, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.inquiries", + "name": "문의 관리", + "name_raw": { + "ko": "문의 관리", + "en": "Inquiry Management" + }, + "description": "상품 1:1 문의 관리 권한", + "description_raw": { + "ko": "상품 1:1 문의 관리 권한", + "en": "Product inquiry management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 13, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 78, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.dashboard", + "name": "대시보드", + "name_raw": { + "ko": "대시보드", + "en": "Dashboard" + }, + "description": "관리자 대시보드 이커머스 영역 조회 권한", + "description_raw": { + "ko": "관리자 대시보드 이커머스 영역 조회 권한", + "en": "Admin dashboard commerce area view permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 14, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 80, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-products", + "name": "사용자 상품", + "name_raw": { + "ko": "사용자 상품", + "en": "User Products" + }, + "description": "사용자 상품 접근 권한 (블랙컨슈머 차단용)", + "description_raw": { + "ko": "사용자 상품 접근 권한 (블랙컨슈머 차단용)", + "en": "User product access permissions (for blocking malicious consumers)" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 15, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 82, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-orders", + "name": "사용자 주문", + "name_raw": { + "ko": "사용자 주문", + "en": "User Orders" + }, + "description": "사용자 주문 관련 권한 (블랙컨슈머 차단용)", + "description_raw": { + "ko": "사용자 주문 관련 권한 (블랙컨슈머 차단용)", + "en": "User order permissions (for blocking malicious consumers)" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 16, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 86, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-reviews", + "name": "사용자 리뷰", + "name_raw": { + "ko": "사용자 리뷰", + "en": "User Reviews" + }, + "description": "사용자 리뷰 관련 권한 (블랙컨슈머 차단용)", + "description_raw": { + "ko": "사용자 리뷰 관련 권한 (블랙컨슈머 차단용)", + "en": "User review permissions (for blocking malicious consumers)" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 17, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 88, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.mileage", + "name": "마일리지 관리", + "name_raw": { + "ko": "마일리지 관리", + "en": "Mileage Management" + }, + "description": "마일리지 내역 조회 및 수동 지급/차감 권한", + "description_raw": { + "ko": "마일리지 내역 조회 및 수동 지급/차감 권한", + "en": "Mileage history and manual grant/deduct permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 18, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 91, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-currency", + "name": "회원 결제 통화 관리", + "name_raw": { + "ko": "회원 결제 통화 관리", + "en": "User Currency Management" + }, + "description": "관리자가 회원별 결제 통화를 변경하는 권한", + "description_raw": { + "ko": "관리자가 회원별 결제 통화를 변경하는 권한", + "en": "Permission for admins to change a user's payment currency" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 19, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 93, + "parent_id": 21, + "identifier": "sirsoft-ecommerce.user-shipping-country", + "name": "회원 배송국가 관리", + "name_raw": { + "ko": "회원 배송국가 관리", + "en": "User Shipping Country Management" + }, + "description": "관리자가 회원별 배송국가를 변경하는 권한", + "description_raw": { + "ko": "관리자가 회원별 배송국가를 변경하는 권한", + "en": "Permission for admins to change a user's shipping country" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-ecommerce", + "resource_route_key": null, + "owner_key": null, + "order": 20, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + } + ] + }, + { + "id": 109, + "parent_id": null, + "identifier": "sirsoft-page", + "name": "페이지", + "name_raw": { + "ko": "페이지", + "en": "Page" + }, + "description": "페이지 모듈 권한", + "description_raw": { + "ko": "페이지 모듈 권한", + "en": "Page module permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "resource_route_key": null, + "owner_key": null, + "order": 100, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 110, + "parent_id": 109, + "identifier": "sirsoft-page.pages", + "name": "페이지 관리", + "name_raw": { + "ko": "페이지 관리", + "en": "Page Management" + }, + "description": "페이지 관리 권한", + "description_raw": { + "ko": "페이지 관리 권한", + "en": "Page management permissions" + }, + "extension_type": "module", + "extension_identifier": "sirsoft-page", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + } + ] + }, + { + "id": 115, + "parent_id": null, + "identifier": "core", + "name": "코어", + "name_raw": { + "ko": "코어", + "en": "Core" + }, + "description": "코어 시스템 권한", + "description_raw": { + "ko": "코어 시스템 권한", + "en": "Core system permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": false, + "children": [ + { + "id": 116, + "parent_id": 115, + "identifier": "core.users", + "name": "사용자 관리", + "name_raw": { + "ko": "사용자 관리", + "en": "User Management" + }, + "description": "사용자 관리 권한", + "description_raw": { + "ko": "사용자 관리 권한", + "en": "User management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 1, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 121, + "parent_id": 115, + "identifier": "core.menus", + "name": "메뉴 관리", + "name_raw": { + "ko": "메뉴 관리", + "en": "Menu Management" + }, + "description": "메뉴 관리 권한", + "description_raw": { + "ko": "메뉴 관리 권한", + "en": "Menu management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 2, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 126, + "parent_id": 115, + "identifier": "core.modules", + "name": "모듈 관리", + "name_raw": { + "ko": "모듈 관리", + "en": "Module Management" + }, + "description": "모듈 관리 권한", + "description_raw": { + "ko": "모듈 관리 권한", + "en": "Module management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 3, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 131, + "parent_id": 115, + "identifier": "core.plugins", + "name": "플러그인 관리", + "name_raw": { + "ko": "플러그인 관리", + "en": "Plugin Management" + }, + "description": "플러그인 관리 권한", + "description_raw": { + "ko": "플러그인 관리 권한", + "en": "Plugin management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 4, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 137, + "parent_id": 115, + "identifier": "core.templates", + "name": "템플릿 관리", + "name_raw": { + "ko": "템플릿 관리", + "en": "Template Management" + }, + "description": "템플릿 관리 권한", + "description_raw": { + "ko": "템플릿 관리 권한", + "en": "Template management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 5, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 143, + "parent_id": 115, + "identifier": "core.permissions", + "name": "권한 관리", + "name_raw": { + "ko": "권한 관리", + "en": "Permission Management" + }, + "description": "역할 및 권한 관리 권한", + "description_raw": { + "ko": "역할 및 권한 관리 권한", + "en": "Role and permission management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 6, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 148, + "parent_id": 115, + "identifier": "core.notification-logs", + "name": "알림 발송 이력", + "name_raw": { + "ko": "알림 발송 이력", + "en": "Notification Logs" + }, + "description": "알림 발송 이력 관리 권한", + "description_raw": { + "ko": "알림 발송 이력 관리 권한", + "en": "Notification log management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 7, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 151, + "parent_id": 115, + "identifier": "core.notifications", + "name": "알림 (관리자)", + "name_raw": { + "ko": "알림 (관리자)", + "en": "Notifications (Admin)" + }, + "description": "관리자용 알림 관리 권한 (관리자 화면에서 사용)", + "description_raw": { + "ko": "관리자용 알림 관리 권한 (관리자 화면에서 사용)", + "en": "Admin notification management permissions (used in admin UI)" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 155, + "parent_id": 115, + "identifier": "core.user-notifications", + "name": "알림 (사용자)", + "name_raw": { + "ko": "알림 (사용자)", + "en": "Notifications (User)" + }, + "description": "사용자용 알림 권한 (사용자 화면에서 본인 알림 관리)", + "description_raw": { + "ko": "사용자용 알림 권한 (사용자 화면에서 본인 알림 관리)", + "en": "User notification permissions (managing own notifications in user UI)" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 159, + "parent_id": 115, + "identifier": "core.identity", + "name": "본인인증 (사용자)", + "name_raw": { + "ko": "본인인증 (사용자)", + "en": "Identity Verification (User)" + }, + "description": "로그인 사용자의 본인인증 challenge 요청/검증/취소 권한", + "description_raw": { + "ko": "로그인 사용자의 본인인증 challenge 요청/검증/취소 권한", + "en": "Permissions for authenticated users to request/verify/cancel IDV challenges" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 163, + "parent_id": 115, + "identifier": "core.admin.identity", + "name": "본인인증 관리 (관리자)", + "name_raw": { + "ko": "본인인증 관리 (관리자)", + "en": "Identity Verification Management (Admin)" + }, + "description": "관리자용 IDV 프로바이더/정책/로그 관리 권한", + "description_raw": { + "ko": "관리자용 IDV 프로바이더/정책/로그 관리 권한", + "en": "Admin permissions for IDV providers, policies, and logs" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 172, + "parent_id": 115, + "identifier": "core.settings", + "name": "환경설정", + "name_raw": { + "ko": "환경설정", + "en": "Settings" + }, + "description": "시스템 환경설정 권한", + "description_raw": { + "ko": "시스템 환경설정 권한", + "en": "System settings permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 8, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 175, + "parent_id": 115, + "identifier": "core.dashboard", + "name": "대시보드", + "name_raw": { + "ko": "대시보드", + "en": "Dashboard" + }, + "description": "대시보드 접근 권한", + "description_raw": { + "ko": "대시보드 접근 권한", + "en": "Dashboard access permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 9, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 181, + "parent_id": 115, + "identifier": "core.activities", + "name": "활동 로그", + "name_raw": { + "ko": "활동 로그", + "en": "Activity Logs" + }, + "description": "활동 로그 조회 권한", + "description_raw": { + "ko": "활동 로그 조회 권한", + "en": "Activity log access permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 10, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 184, + "parent_id": 115, + "identifier": "core.attachments", + "name": "첨부파일 관리", + "name_raw": { + "ko": "첨부파일 관리", + "en": "Attachment Management" + }, + "description": "첨부파일 관리 권한", + "description_raw": { + "ko": "첨부파일 관리 권한", + "en": "Attachment management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 11, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 188, + "parent_id": 115, + "identifier": "core.schedules", + "name": "스케줄 관리", + "name_raw": { + "ko": "스케줄 관리", + "en": "Schedule Management" + }, + "description": "스케줄 작업 관리 권한", + "description_raw": { + "ko": "스케줄 작업 관리 권한", + "en": "Schedule task management permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 12, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + }, + { + "id": 194, + "parent_id": 115, + "identifier": "core.language_packs", + "name": "언어팩 관리", + "name_raw": { + "ko": "언어팩 관리", + "en": "Language Pack Management" + }, + "description": "언어팩 설치/제거/활성화 권한", + "description_raw": { + "ko": "언어팩 설치/제거/활성화 권한", + "en": "Language pack install/uninstall/activation permissions" + }, + "extension_type": "core", + "extension_identifier": "core", + "resource_route_key": null, + "owner_key": null, + "order": 13, + "is_assigned": true, + "is_assignable": false, + "scope_type": null + } + ] + } + ], + "created_at": "2026-07-08 10:41:24", + "updated_at": "2026-07-08 10:41:24", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_toggle_status": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -254,9 +9280,87 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.role.update_validation_rules`). +**요청 예시** + +```http +PUT /api/admin/roles/4 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": "예시 이름", + "description": "예시 내용입니다.", + "is_active": true, + "permissions": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `4` | 기본 키 (내부 식별자) | +| identifier | string | `apidoc-sample-role` | 역할명 (예: admin, user, manager) | +| name | string | `실측 예시값` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| name_raw | object | `{"ko":"실측 예시값","en":"실측 예시값","fr":"실측 예시값"}` | `name` 의 원본 값 (현재 로케일 미해석 원본 JSON/문자열) | +| description | string | `실측 예시값` | 설명 (다국어 필드는 로케일별 값 객체) | +| description_raw | object | `{"ko":"실측 예시값","en":"실측 예시값","fr":"실측 예시값"}` | `description` 의 원본 값 (현재 로케일 미해석 원본 JSON/문자열) | +| extension_type | null | `null` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) | +| extension_identifier | null | `null` | 이 리소스를 소유한 확장의 식별자 | +| extension_name | null | `null` | 이 리소스를 소유한 확장의 표시 이름 (manifest name) | +| is_deletable | boolean | `true` | deletable 여부 | +| is_active | boolean | `true` | active 여부 | +| created_at | string | `2026-07-08 10:41:24` | 생성 일시 | +| updated_at | string | `2026-07-08 12:14:48` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "역할이 성공적으로 수정되었습니다.", + "data": { + "id": 4, + "identifier": "apidoc-sample-role", + "name": "실측 예시값", + "name_raw": { + "ko": "실측 예시값", + "en": "실측 예시값", + "fr": "실측 예시값" + }, + "description": "실측 예시값", + "description_raw": { + "ko": "실측 예시값", + "en": "실측 예시값", + "fr": "실측 예시값" + }, + "extension_type": null, + "extension_identifier": null, + "extension_name": null, + "is_deletable": true, + "is_active": true, + "created_at": "2026-07-08 10:41:24", + "updated_at": "2026-07-08 12:14:48", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_toggle_status": true + } + } +} +``` **에러 응답** @@ -286,9 +9390,77 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | role | path | string | 예 | — | 대상 role의 식별자 | +**요청 예시** + +```http +PATCH /api/admin/roles/4/toggle-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `4` | 기본 키 (내부 식별자) | +| identifier | string | `apidoc-sample-role` | 역할명 (예: admin, user, manager) | +| name | string | `API 문서 샘플 역할` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| name_raw | object | `{"ko":"API 문서 샘플 역할","en":"API Doc Sample Role"}` | `name` 의 원본 값 (현재 로케일 미해석 원본 JSON/문자열) | +| description | string | `문서 실측용 역할` | 설명 (다국어 필드는 로케일별 값 객체) | +| description_raw | object | `{"ko":"문서 실측용 역할","en":"Sample role for API docs"}` | `description` 의 원본 값 (현재 로케일 미해석 원본 JSON/문자열) | +| extension_type | null | `null` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) | +| extension_identifier | null | `null` | 이 리소스를 소유한 확장의 식별자 | +| extension_name | null | `null` | 이 리소스를 소유한 확장의 표시 이름 (manifest name) | +| is_deletable | boolean | `true` | deletable 여부 | +| is_active | boolean | `false` | active 여부 | +| users_count | integer | `1` | users 개수 (집계) | +| created_at | string | `2026-07-08 10:41:24` | 생성 일시 | +| updated_at | string | `2026-07-08 12:14:48` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "역할이 성공적으로 수정되었습니다.", + "data": { + "id": 4, + "identifier": "apidoc-sample-role", + "name": "API 문서 샘플 역할", + "name_raw": { + "ko": "API 문서 샘플 역할", + "en": "API Doc Sample Role" + }, + "description": "문서 실측용 역할", + "description_raw": { + "ko": "문서 실측용 역할", + "en": "Sample role for API docs" + }, + "extension_type": null, + "extension_identifier": null, + "extension_name": null, + "is_deletable": true, + "is_active": false, + "users_count": 1, + "created_at": "2026-07-08 10:41:24", + "updated_at": "2026-07-08 12:14:48", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_toggle_status": true + } + } +} +``` **에러 응답** diff --git a/docs/backend/api/schedules.md b/docs/backend/api/schedules.md index b4982b9f..b3ae70cb 100644 --- a/docs/backend/api/schedules.md +++ b/docs/backend/api/schedules.md @@ -45,6 +45,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.schedule.list_validation_rules`). +**요청 예시** + +```http +GET /api/admin/schedules?page=1&per_page=1&filters=%EC%98%88%EC%8B%9C%EA%B0%92&type=artisan&frequency=everyMinute&status=active&last_result=success&without_overlapping=0&run_in_maintenance=0&extension_type=core&extension_identifier=example-key&created_from=2026-01-01&created_to=2026-01-01&sort_by=created_at&sort_order=asc HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -71,6 +80,70 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | created_at | string | `2026-07-06` | 생성 일시 | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "스케줄 목록을 조회했습니다.", + "data": { + "data": [ + { + "id": 1, + "name": "API 문서 샘플 스케줄", + "type": "artisan", + "type_label": "Artisan 커맨드", + "command": "cache:clear", + "expression": "0 3 * * *", + "frequency": "daily", + "frequency_label": "매일", + "without_overlapping": true, + "run_in_maintenance": false, + "is_active": true, + "last_result": "success", + "last_result_label": "성공", + "last_run_at": "2026-07-07T10:41:24+09:00", + "last_duration": null, + "next_run_at": "2026-07-08T12:00:00+09:00", + "creator": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자" + }, + "created_at": "2026-07-08", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_run": true + } + } + ], + "statistics": { + "total": 1, + "active": 1, + "inactive": 0, + "success": 1, + "failed": 0, + "running": 0, + "never_run": 0 + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -109,9 +182,104 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.schedule.create_validation_rules`). +**요청 예시** + +```http +POST /api/admin/schedules HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": "예시 이름", + "description": "예시 내용입니다.", + "type": "artisan", + "command": "예시값", + "expression": "예시값", + "frequency": "everyMinute", + "without_overlapping": true, + "run_in_maintenance": true, + "timeout": 1, + "is_active": true, + "extension_type": "core", + "extension_identifier": "example-key" +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `14` | 기본 키 (내부 식별자) | +| name | string | `실측 예시값` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| description | string | `실측 예시값` | 설명 (다국어 필드는 로케일별 값 객체) | +| type | string | `artisan` | 작업 유형: artisan(Artisan 커맨드), shell(쉘 명령), url(URL 호출) | +| type_label | string | `Artisan 커맨드` | `type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| command | string | `실측 예시값` | 명령어 또는 URL | +| expression | string | `실측 예시값` | Cron 표현식 | +| frequency | string | `everyMinute` | 실행 주기: everyMinute(매분), hourly(매시간), daily(매일), weekly(매주), monthly(매월), custom(사용자 정의) | +| frequency_label | string | `매분` | `frequency` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| without_overlapping | boolean | `true` | 중복 실행 방지 여부: 0(허용), 1(방지) | +| run_in_maintenance | boolean | `true` | 점검 모드 실행 여부: 0(비실행), 1(실행) | +| timeout | integer | `1` | 실행 제한 시간 (초) | +| is_active | boolean | `true` | active 여부 | +| last_result | string | `never` | 마지막 실행 결과: success(성공), failed(실패), running(실행중), never(미실행) | +| last_result_label | string | `미실행` | `last_result` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| last_duration | null | `null` | 마지막 실행 소요 시간 (초/밀리초 — 실행 이력 파생) | +| extension_type | string | `core` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) | +| extension_identifier | string | `probe_6a4dc0a862b69` | 이 리소스를 소유한 확장의 식별자 | +| creator | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| created_at | string | `2026-07-08 12:14:48` | 생성 일시 | +| updated_at | string | `2026-07-08 12:14:48` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 201 +``` + +```json +{ + "success": true, + "message": "스케줄이 생성되었습니다.", + "data": { + "id": 14, + "name": "실측 예시값", + "description": "실측 예시값", + "type": "artisan", + "type_label": "Artisan 커맨드", + "command": "실측 예시값", + "expression": "실측 예시값", + "frequency": "everyMinute", + "frequency_label": "매분", + "without_overlapping": true, + "run_in_maintenance": true, + "timeout": 1, + "is_active": true, + "last_result": "never", + "last_result_label": "미실행", + "last_duration": null, + "extension_type": "core", + "extension_identifier": "probe_6a4dc0a862b69", + "creator": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자" + }, + "created_at": "2026-07-08 12:14:48", + "updated_at": "2026-07-08 12:14:48", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_run": true + } + } +} +``` **에러 응답** @@ -140,10 +308,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.schedule.bulk_delete_validation_rules`). +**요청 예시** + +```http +DELETE /api/admin/schedules/bulk?ids=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -172,10 +353,31 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.schedule.bulk_update_status_validation_rules`). +**요청 예시** + +```http +PATCH /api/admin/schedules/bulk-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "is_active": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -201,10 +403,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | historyId | path | string | 예 | — | 대상 history의 식별자 | +**요청 예시** + +```http +DELETE /api/admin/schedules/history/{historyId} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -228,6 +443,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/schedules/statistics HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -242,6 +466,28 @@ _단건 응답: `data` 객체의 필드._ | running | integer | `0` | 마지막 실행 결과가 실행중(running)인 스케줄 수 | | never_run | integer | `0` | 아직 한 번도 실행되지 않은(last_result=never) 스케줄 수 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "스케줄 통계를 조회했습니다.", + "data": { + "total": 1, + "active": 1, + "inactive": 0, + "success": 1, + "failed": 0, + "running": 0, + "never_run": 0 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -266,9 +512,34 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | schedule | path | string | 예 | — | 대상 schedule의 식별자 | +**요청 예시** + +```http +DELETE /api/admin/schedules/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "스케줄이 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -295,6 +566,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | schedule | path | string | 예 | — | 대상 schedule의 식별자 | +**요청 예시** + +```http +GET /api/admin/schedules/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -326,6 +606,53 @@ _단건 응답: `data` 객체의 필드._ | updated_at | string | `2026-07-06 19:20:23` | 최종 수정 일시 | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "스케줄 목록을 조회했습니다.", + "data": { + "id": 1, + "name": "API 문서 샘플 스케줄", + "description": "문서 실측용 스케줄", + "type": "artisan", + "type_label": "Artisan 커맨드", + "command": "cache:clear", + "expression": "0 3 * * *", + "frequency": "daily", + "frequency_label": "매일", + "without_overlapping": true, + "run_in_maintenance": false, + "timeout": 300, + "is_active": true, + "last_result": "success", + "last_result_label": "성공", + "last_run_at": "2026-07-07T10:41:24+09:00", + "last_duration": null, + "next_run_at": "2026-07-08T12:00:00+09:00", + "extension_type": null, + "extension_identifier": null, + "creator": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자" + }, + "created_at": "2026-07-08 10:41:24", + "updated_at": "2026-07-08 10:41:24", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_run": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -365,9 +692,106 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.schedule.update_validation_rules`). +**요청 예시** + +```http +PUT /api/admin/schedules/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": "예시 이름", + "description": "예시 내용입니다.", + "type": "artisan", + "command": "예시값", + "expression": "예시값", + "frequency": "everyMinute", + "without_overlapping": true, + "run_in_maintenance": true, + "timeout": 1, + "is_active": true, + "extension_type": "core", + "extension_identifier": "example-key" +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | string | `실측 예시값` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| description | string | `실측 예시값` | 설명 (다국어 필드는 로케일별 값 객체) | +| type | string | `artisan` | 작업 유형: artisan(Artisan 커맨드), shell(쉘 명령), url(URL 호출) | +| type_label | string | `Artisan 커맨드` | `type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| command | string | `실측 예시값` | 명령어 또는 URL | +| expression | string | `실측 예시값` | Cron 표현식 | +| frequency | string | `everyMinute` | 실행 주기: everyMinute(매분), hourly(매시간), daily(매일), weekly(매주), monthly(매월), custom(사용자 정의) | +| frequency_label | string | `매분` | `frequency` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| without_overlapping | boolean | `true` | 중복 실행 방지 여부: 0(허용), 1(방지) | +| run_in_maintenance | boolean | `true` | 점검 모드 실행 여부: 0(비실행), 1(실행) | +| timeout | integer | `1` | 실행 제한 시간 (초) | +| is_active | boolean | `true` | active 여부 | +| last_result | string | `success` | 마지막 실행 결과: success(성공), failed(실패), running(실행중), never(미실행) | +| last_result_label | string | `성공` | `last_result` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| last_run_at | string | `2026-07-07T10:41:24+09:00` | last run 일시 | +| last_duration | null | `null` | 마지막 실행 소요 시간 (초/밀리초 — 실행 이력 파생) | +| extension_type | string | `core` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) | +| extension_identifier | string | `probe_6a4dc0a8e44c0` | 이 리소스를 소유한 확장의 식별자 | +| creator | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| created_at | string | `2026-07-08 10:41:24` | 생성 일시 | +| updated_at | string | `2026-07-08 12:14:48` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "스케줄이 수정되었습니다.", + "data": { + "id": 1, + "name": "실측 예시값", + "description": "실측 예시값", + "type": "artisan", + "type_label": "Artisan 커맨드", + "command": "실측 예시값", + "expression": "실측 예시값", + "frequency": "everyMinute", + "frequency_label": "매분", + "without_overlapping": true, + "run_in_maintenance": true, + "timeout": 1, + "is_active": true, + "last_result": "success", + "last_result_label": "성공", + "last_run_at": "2026-07-07T10:41:24+09:00", + "last_duration": null, + "extension_type": "core", + "extension_identifier": "probe_6a4dc0a8e44c0", + "creator": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자" + }, + "created_at": "2026-07-08 10:41:24", + "updated_at": "2026-07-08 12:14:48", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_run": true + } + } +} +``` **에러 응답** @@ -395,9 +819,83 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | schedule | path | string | 예 | — | 대상 schedule의 식별자 | +**요청 예시** + +```http +POST /api/admin/schedules/1/duplicate HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `15` | 기본 키 (내부 식별자) | +| name | string | `API 문서 샘플 스케줄 (복사본)` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| description | string | `문서 실측용 스케줄` | 설명 (다국어 필드는 로케일별 값 객체) | +| type | string | `artisan` | 작업 유형: artisan(Artisan 커맨드), shell(쉘 명령), url(URL 호출) | +| type_label | string | `Artisan 커맨드` | `type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| command | string | `cache:clear` | 명령어 또는 URL | +| expression | string | `0 3 * * *` | Cron 표현식 | +| frequency | string | `daily` | 실행 주기: everyMinute(매분), hourly(매시간), daily(매일), weekly(매주), monthly(매월), custom(사용자 정의) | +| frequency_label | string | `매일` | `frequency` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| without_overlapping | boolean | `true` | 중복 실행 방지 여부: 0(허용), 1(방지) | +| run_in_maintenance | boolean | `false` | 점검 모드 실행 여부: 0(비실행), 1(실행) | +| timeout | integer | `300` | 실행 제한 시간 (초) | +| is_active | boolean | `false` | active 여부 | +| last_result | string | `never` | 마지막 실행 결과: success(성공), failed(실패), running(실행중), never(미실행) | +| last_result_label | string | `미실행` | `last_result` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| last_duration | null | `null` | 마지막 실행 소요 시간 (초/밀리초 — 실행 이력 파생) | +| extension_type | null | `null` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) | +| extension_identifier | null | `null` | 이 리소스를 소유한 확장의 식별자 | +| created_at | string | `2026-07-08 12:14:48` | 생성 일시 | +| updated_at | string | `2026-07-08 12:14:48` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 201 +``` + +```json +{ + "success": true, + "message": "스케줄이 복제되었습니다.", + "data": { + "id": 15, + "name": "API 문서 샘플 스케줄 (복사본)", + "description": "문서 실측용 스케줄", + "type": "artisan", + "type_label": "Artisan 커맨드", + "command": "cache:clear", + "expression": "0 3 * * *", + "frequency": "daily", + "frequency_label": "매일", + "without_overlapping": true, + "run_in_maintenance": false, + "timeout": 300, + "is_active": false, + "last_result": "never", + "last_result_label": "미실행", + "last_duration": null, + "extension_type": null, + "extension_identifier": null, + "created_at": "2026-07-08 12:14:48", + "updated_at": "2026-07-08 12:14:48", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_run": true + } + } +} +``` **에러 응답** @@ -434,12 +932,46 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.schedule.history_list_validation_rules`). +**요청 예시** + +```http +GET /api/admin/schedules/1/history?page=1&per_page=1&status=success&trigger_type=scheduled&started_from=2026-01-01&started_to=2026-01-01&sort_by=started_at&sort_order=asc HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "실행 이력을 조회했습니다.", + "data": { + "data": [], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 0, + "from": null, + "to": null, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -466,9 +998,70 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | schedule | path | string | 예 | — | 대상 schedule의 식별자 | +**요청 예시** + +```http +POST /api/admin/schedules/1/run HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `7` | 기본 키 (내부 식별자) | +| schedule_id | integer | `1` | schedule 식별자 (연관 리소스 참조) | +| started_at | string | `2026-07-08T12:14:49+09:00` | started 일시 | +| ended_at | string | `2026-07-08T12:14:49+09:00` | ended 일시 | +| duration | integer | `0` | 실행 소요 시간 (초/밀리초) | +| duration_formatted | null | `null` | `duration` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| status | string | `success` | 상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조) | +| status_label | string | `성공` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| exit_code | integer | `0` | 실행 종료 코드 (0=성공, 그 외=실패) | +| memory_usage | integer | `427352` | 실행 중 최대 메모리 사용량 (바이트) | +| memory_usage_formatted | string | `417.34 KB` | `memory_usage` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| output | string | ` INFO Application cache cleared s…` | 실행 표준 출력 내용 | +| error_output | null | `null` | 실행 표준 에러 출력 내용 (없으면 빈 문자열/null) | +| trigger_type | string | `manual` | 동작을 유발한 방식/주체 구분 값 | +| trigger_type_label | string | `수동 실행` | `trigger_type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| created_at | string | `2026-07-08 12:14:49` | 생성 일시 | +| updated_at | string | `2026-07-08 12:14:49` | 최종 수정 일시 | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "스케줄이 실행되었습니다.", + "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": 427352, + "memory_usage_formatted": "417.34 KB", + "output": "\n INFO Application cache cleared successfully. \n\r\n", + "error_output": null, + "trigger_type": "manual", + "trigger_type_label": "수동 실행", + "created_at": "2026-07-08 12:14:49", + "updated_at": "2026-07-08 12:14:49" + } +} +``` **에러 응답** diff --git a/docs/backend/api/search.md b/docs/backend/api/search.md index e74626a3..b1ee332b 100644 --- a/docs/backend/api/search.md +++ b/docs/backend/api/search.md @@ -37,6 +37,14 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.search.validation_rules`). +**요청 예시** + +```http +GET /api/search?q=%EC%98%88%EC%8B%9C%EA%B0%92&type=%EC%98%88%EC%8B%9C%EA%B0%92&sort=relevance&page=1&per_page=1&board_slug=example-key&category_id=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -46,6 +54,23 @@ _단건 응답: `data` 객체의 필드._ | q | string | `` | 실제 검색에 사용된 검색어 (요청 `q` 를 trim 하여 에코, 검색어가 비어 있으면 빈 문자열) | | total | integer | `0` | 전체 개수 (집계) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "검색어를 입력해주세요.", + "data": { + "q": "", + "total": 0 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/seo.md b/docs/backend/api/seo.md index bd566602..1d2185ff 100644 --- a/docs/backend/api/seo.md +++ b/docs/backend/api/seo.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/seo/cached-urls HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -36,6 +45,23 @@ _단건 응답: `data` 객체의 필드._ | urls | array | `[]` | 현재 사전 렌더 캐시에 남아 있는 봇 대상 페이지 URL 목록 (SeoCacheManager 인덱스에서 조회). | | count | integer | `0` | 캐시된 URL 개수 (`urls` 배열 길이). | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "urls": [], + "count": 0 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -63,10 +89,29 @@ SeoCacheManager 인덱스에서 현재 캐시된 SEO 페이지 URL 목록과 개 | layout | body | string | 아니오 | — | 무효화 대상 레이아웃명. 지정 시 해당 레이아웃의 SEO 캐시만 삭제하고 무효화된 항목 수를 반환하며, 미지정 시 전체 SEO 캐시를 삭제한다. | | module | body | string | 아니오 | — | 모듈 식별자 필터. 검증 규칙에는 정의되어 있으나 현재 컨트롤러 로직에서는 사용되지 않는다(향후 모듈 단위 캐시 무효화 확장 예약 필드). | +**요청 예시** + +```http +POST /api/admin/seo/clear-cache HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "layout": "예시값", + "module": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -92,9 +137,42 @@ SEO 사전 렌더 캐시를 삭제합니다. `layout` 을 지정하면 해당 _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/seo/sitemap/regenerate HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| last_updated_at | string | `2026-07-08T03:14:49+00:00` | last updated 일시 | +| size_bytes | integer | `25489` | 크기 (바이트) | +| ttl | integer | `86400` | 캐시 유효 시간 (Time To Live, 초) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "Sitemap 재생성이 완료되었습니다.", + "data": { + "last_updated_at": "2026-07-08T03:14:49+00:00", + "size_bytes": 25489, + "ttl": 86400 + } +} +``` **에러 응답** @@ -120,6 +198,15 @@ sitemap.xml 을 즉시 재생성합니다. SitemapManager 에 위임하며 큐 _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/seo/stats HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -130,6 +217,30 @@ _단건 응답: `data` 객체의 필드._ | by_layout | array | `[]` | 레이아웃별 통계 목록 (`layout_name` 으로 그룹핑). 각 원소는 `layout_name`·`total`·`hits`·`misses`·`hit_rate`·`avg_response_time_ms` 를 가지며, 레이아웃 단위로 캐시 효율을 비교하는 용도. | | by_module | array | `[]` | 모듈별 통계 목록 (`module_identifier` 로 그룹핑). 각 원소는 `module_identifier`·`total`·`hits`·`misses`·`hit_rate`·`avg_response_time_ms` 를 가지며, 어느 확장 모듈의 SEO 페이지가 캐시로 재사용되는지 파악하는 용도. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "overall": { + "total_entries": 0, + "hits": 0, + "misses": 0, + "hit_rate": 0, + "avg_response_time_ms": null + }, + "by_layout": [], + "by_module": [] + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -154,10 +265,23 @@ SEO 캐시 적중 현황을 최근 7일 기준으로 전체·레이아웃별· _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/seo/warmup HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/settings.md b/docs/backend/api/settings.md index a12de377..cd71a7ec 100644 --- a/docs/backend/api/settings.md +++ b/docs/backend/api/settings.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/settings HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -49,6 +58,368 @@ _단건 응답: `data` 객체의 필드._ | available_drivers | object | `{"storage":[{"id":"local","label":{"ko":"로컬","en":"Local"…` | 드라이버 선택지 카탈로그 (DriverRegistryService 산물). 종류별(storage/cache/session/queue 등) 선택 가능한 드라이버 목록을 id/다국어 label 형태로 제공 | | abilities | object | `{"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "설정을 성공적으로 가져왔습니다.", + "data": { + "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": "{MASKED}", + "max_login_attempts": 5, + "login_lockout_time": 30, + "two_factor_auth": false, + "password_min_length": "{MASKED}", + "require_password_special_char": "{MASKED}" + }, + "mail": { + "mailer": "smtp", + "host": "", + "port": 587, + "username": "", + "password": "{MASKED}", + "encryption": "tls", + "mailgun_domain": "", + "mailgun_secret": "{MASKED}", + "mailgun_endpoint": "api.mailgun.net", + "ses_key": "", + "ses_secret": "{MASKED}", + "ses_region": "ap-northeast-2", + "from_address": "heuristing@gmail.com", + "from_name": "그누보드7" + }, + "upload": { + "max_file_size": 10, + "allowed_extensions": [ + "jpg", + "jpeg", + "png", + "gif", + "webp", + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "zip" + ], + "image_max_width": 2000, + "image_max_height": 2000, + "image_quality": 85 + }, + "seo": { + "meta_title_suffix": "", + "meta_description": "", + "meta_keywords": "", + "google_analytics_id": "", + "google_site_verification": "", + "naver_site_verification": "", + "bot_user_agents": [ + "kakaotalk-scrap", + "Meta-ExternalAgent", + "ChatGPT-User" + ], + "bot_detection_enabled": true, + "bot_detection_library_enabled": true, + "og_default_site_name": "", + "og_image_default_width": 1200, + "og_image_default_height": 630, + "twitter_default_card": "summary_large_image", + "twitter_default_site": "", + "cache_enabled": true, + "cache_ttl": 7200, + "sitemap_enabled": true, + "sitemap_cache_ttl": 86400, + "sitemap_schedule": "daily", + "sitemap_schedule_time": "02:00", + "sitemap_last_updated_at": "2026-07-08T03:14:49+00:00", + "generator_enabled": true, + "generator_content": "" + }, + "advanced": { + "cache_enabled": true, + "cache_default_ttl": 86400, + "layout_cache_enabled": true, + "layout_cache_ttl": 3600, + "seo_cache_enabled": true, + "seo_cache_ttl": 7200, + "seo_sitemap_cache_ttl": 86400, + "stats_cache_enabled": true, + "stats_cache_ttl": 1800, + "notification_cache_ttl": 3600, + "extension_status_cache_ttl": 86400, + "geoip_cache_enabled": true, + "geoip_cache_ttl": 86400, + "version_check_cache_ttl": 3600, + "debug_mode": true, + "sql_query_log": false, + "log_level": "error", + "core_update_github_url": "https://github.com/custom/repo", + "core_update_github_token": "{MASKED}", + "geoip_enabled": false, + "geoip_license_key": null, + "geoip_auto_update_enabled": true, + "geoip_last_updated_at": "" + }, + "cache": { + "cache_enabled": true, + "cache_default_ttl": 86400, + "layout_cache_enabled": true, + "layout_cache_ttl": 3600, + "seo_cache_enabled": true, + "seo_cache_ttl": 7200, + "seo_sitemap_cache_ttl": 86400, + "stats_cache_enabled": true, + "stats_cache_ttl": 1800, + "notification_cache_ttl": 3600, + "extension_status_cache_ttl": 86400, + "geoip_cache_enabled": true, + "geoip_cache_ttl": 86400, + "version_check_cache_ttl": 3600 + }, + "debug": { + "debug_mode": true, + "sql_query_log": false, + "log_level": "error" + }, + "drivers": { + "storage_driver": "local", + "s3_bucket": null, + "s3_region": "ap-northeast-2", + "s3_access_key": null, + "s3_secret_key": null, + "s3_url": null, + "cache_driver": "file", + "redis_host": "127.0.0.1", + "redis_port": 6379, + "redis_password": null, + "redis_database": "1", + "memcached_host": "127.0.0.1", + "memcached_port": 11211, + "session_driver": "file", + "session_lifetime": 120, + "queue_driver": "sync", + "log_driver": "daily", + "log_level": "error", + "log_days": 14, + "websocket_enabled": true, + "websocket_app_id": "test-app-id", + "websocket_app_key": "{MASKED}", + "websocket_app_secret": "{MASKED}", + "websocket_host": "localhost", + "websocket_port": 8080, + "websocket_scheme": "https", + "websocket_verify_ssl": true, + "websocket_server_host": "127.0.0.1", + "websocket_server_port": 8080, + "websocket_server_scheme": "http", + "search_engine_driver": "mysql-fulltext" + }, + "core_update": { + "core_update_github_url": "https://github.com/custom/repo", + "core_update_github_token": "{MASKED}" + }, + "geoip": { + "geoip_enabled": false, + "geoip_license_key": null, + "geoip_auto_update_enabled": true, + "geoip_last_updated_at": "" + }, + "notifications": { + "channels": [ + { + "id": "mail", + "is_active": true, + "sort_order": 1 + }, + { + "id": "database", + "is_active": true, + "sort_order": 2 + } + ] + }, + "identity": { + "default_provider": "g7:core.mail", + "purpose_providers": { + "signup": null, + "password_reset": null, + "inicis": { + "adult_verification": "inicis" + } + }, + "challenge_ttl_minutes": 15, + "max_attempts": 5 + }, + "available_drivers": { + "storage": [ + { + "id": "local", + "label": { + "ko": "로컬", + "en": "Local", + "fr": "로컬" + } + }, + { + "id": "s3", + "label": { + "ko": "Amazon S3", + "en": "Amazon S3", + "fr": "Amazon S3" + } + } + ], + "cache": [ + { + "id": "file", + "label": { + "ko": "파일", + "en": "File", + "fr": "파일" + } + }, + { + "id": "redis", + "label": { + "ko": "Redis", + "en": "Redis", + "fr": "Redis" + } + } + ], + "session": [ + { + "id": "file", + "label": { + "ko": "파일", + "en": "File", + "fr": "파일" + } + }, + { + "id": "database", + "label": { + "ko": "데이터베이스", + "en": "Database", + "fr": "데이터베이스" + } + }, + { + "id": "redis", + "label": { + "ko": "Redis", + "en": "Redis", + "fr": "Redis" + } + } + ], + "queue": [ + { + "id": "sync", + "label": { + "ko": "동기", + "en": "Sync", + "fr": "동기" + } + }, + { + "id": "database", + "label": { + "ko": "데이터베이스", + "en": "Database", + "fr": "데이터베이스" + } + }, + { + "id": "redis", + "label": { + "ko": "Redis", + "en": "Redis", + "fr": "Redis" + } + } + ], + "log": [ + { + "id": "single", + "label": { + "ko": "단일 파일", + "en": "Single File", + "fr": "단일 파일" + } + }, + { + "id": "daily", + "label": { + "ko": "일별 파일", + "en": "Daily File", + "fr": "일별 파일" + } + } + ], + "websocket": [ + { + "id": "reverb", + "label": { + "ko": "Laravel Reverb", + "en": "Laravel Reverb", + "fr": "Laravel Reverb" + } + } + ], + "mail": [ + { + "id": "smtp", + "label": { + "ko": "SMTP", + "en": "SMTP", + "fr": "SMTP" + } + }, + { + "id": "mailgun", + "label": { + "ko": "Mailgun", + "en": "Mailgun", + "fr": "Mailgun" + } + }, + { + "id": "ses", + "label": { + "ko": "SES (Amazon)", + "en": "SES (Amazon)", + "fr": "SES (Amazon)" + } + } + ] + }, + "abilities": { + "can_update": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -86,10 +457,55 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.save_validation_rules`, `core.search.engine_drivers`). +**요청 예시** + +```http +POST /api/admin/settings HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "_tab": "예시값", + "general": [ + "예시값" + ], + "mail": [ + "예시값" + ], + "upload": [ + "예시값" + ], + "seo": [ + "예시값" + ], + "security": [ + "예시값" + ], + "drivers": [ + "예시값" + ], + "advanced": [ + "예시값" + ], + "notifications": [ + "예시값" + ], + "identity": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -115,6 +531,15 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/settings/app-key HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -123,6 +548,22 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | app_key | string | `base64:97gZH*************************…` | 현재 애플리케이션 키(`APP_KEY`)를 마스킹한 문자열. 앞부분 일부만 노출하고 나머지는 별표로 가려 전체 원문은 반환하지 않음 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "성공적으로 처리되었습니다.", + "data": { + "app_key": "{MASKED}" + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -147,10 +588,23 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/settings/backup HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -175,10 +629,23 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/settings/backup-database HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -203,10 +670,23 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/settings/clear-cache HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -231,10 +711,23 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/settings/geoip/update HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -259,9 +752,34 @@ MaxMind GeoLite2-City DB 를 즉시 재다운로드합니다. GeoIpDatabaseServi _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/settings/optimize-system HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "시스템이 성공적으로 최적화되었습니다.", + "data": null +} +``` **에러 응답** @@ -291,10 +809,28 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.regenerate_app_key_validation_rules`). +**요청 예시** + +```http +POST /api/admin/settings/regenerate-app-key HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "password": "Password123!" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -324,10 +860,28 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.restore_validation_rules`). +**요청 예시** + +```http +POST /api/admin/settings/restore HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "backup_path": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -353,6 +907,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/settings/system-info HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -382,6 +945,86 @@ _단건 응답: `data` 객체의 필드._ | timezone | string | `UTC` | 애플리케이션 기본 타임존 (`config('app.timezone')`) | | server_time | string | `2026-07-07 05:08:58` | 서버 현재 시각 (Y-m-d H:i:s) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "성공적으로 처리되었습니다.", + "data": { + "os_info": "Windows NT 10.0", + "web_server": "Apache/2.4.62 (Win64) OpenSSL/3.0.16 PHP/8.3.26", + "php_version": "8.3.26", + "mysql_version": "Mysql 8.4.3", + "g7_version": "7.0.1", + "g7_release_year": "2026", + "laravel_version": "12.54.1", + "environment": "local", + "cpu_info": "Intel(R) Core(TM) Ultra 5 225H", + "memory_usage": { + "total": "31.49 GB", + "used": "26.92 GB", + "free": "4.57 GB", + "percentage": 85.49 + }, + "disk_usage": { + "total": "474.72 GB", + "used": "362.59 GB", + "free": "112.12 GB", + "percentage": 76.38 + }, + "php_memory_limit": "512M", + "max_execution_time": "36000초", + "upload_max_filesize": "2G", + "install_path": "C:\\Users\\HeuJung\\htdocs\\g7_2", + "config_path": "C:\\Users\\HeuJung\\htdocs\\g7_2\\storage\\app/settings", + "log_path": "C:\\Users\\HeuJung\\htdocs\\g7_2\\storage\\logs", + "upload_path": "C:\\Users\\HeuJung\\htdocs\\g7_2\\storage\\app/public", + "php_extensions": { + "required": { + "openssl": true, + "pdo": true, + "mbstring": true, + "tokenizer": "{MASKED}", + "xml": true, + "curl": true, + "json": true, + "zip": true, + "fileinfo": true, + "bcmath": true + }, + "optional": { + "gd": true, + "imagick": false, + "redis": true, + "memcached": false, + "sodium": true, + "exif": true, + "intl": true, + "ldap": false, + "zlib": true + } + }, + "database_config": { + "has_read_write_split": false, + "write": { + "host": "localhost", + "port": 3306, + "database": "g7_2", + "username": "g7_2" + }, + "read": [] + }, + "timezone": "UTC", + "server_time": "2026-07-08 03:14:54" + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -429,10 +1072,47 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.test_driver_connection_validation_rules`). +**요청 예시** + +```http +POST /api/admin/settings/test-driver HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "storage_driver": "예시값", + "cache_driver": "예시값", + "session_driver": "예시값", + "queue_driver": "예시값", + "websocket_enabled": true, + "s3_bucket": "예시값", + "s3_region": "예시값", + "s3_access_key": "예시값", + "s3_secret_key": "예시값", + "s3_url": "https://example.com", + "redis_host": "예시값", + "redis_port": 1, + "redis_password": "Password123!", + "redis_database": 1, + "memcached_host": "예시값", + "memcached_port": 1, + "websocket_app_key": "예시값", + "websocket_host": "예시값", + "websocket_port": 1, + "websocket_scheme": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -476,10 +1156,42 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.test_mail_validation_rules`). +**요청 예시** + +```http +POST /api/admin/settings/test-mail HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "to_email": "user@example.com", + "mailer": "smtp", + "from_address": "user@example.com", + "from_name": "예시 이름", + "host": "예시값", + "port": 1, + "username": "예시 이름", + "password": "Password123!", + "encryption": "tls", + "mailgun_domain": "예시값", + "mailgun_secret": "예시값", + "mailgun_endpoint": "예시값", + "ses_key": "예시값", + "ses_secret": "예시값", + "ses_region": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -507,10 +1219,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | key | path | string | 예 | — | 대상 설정/항목의 키 | +**요청 예시** + +```http +GET /api/admin/settings/{key} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -541,10 +1266,28 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.update_validation_rules`). +**요청 예시** + +```http +PUT /api/admin/settings/{key} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "value": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/templates.md b/docs/backend/api/templates.md index da840f0a..669e3407 100644 --- a/docs/backend/api/templates.md +++ b/docs/backend/api/templates.md @@ -37,6 +37,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.index_validation_rules`). +**요청 예시** + +```http +GET /api/admin/templates?type=user&search=%EC%98%88%EC%8B%9C%EA%B0%92&filters=%EC%98%88%EC%8B%9C%EA%B0%92&status=installed&per_page=1&page=1&include_hidden=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -65,6 +74,125 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | incompatible_required_version | null | `null` | 요구 코어 버전 미충족 시 필요한 버전 (호환되면 null) | | abilities | object | `{"can_install":true,"can_activate":true,"can_uninstall":t…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "템플릿 정보를 성공적으로 가져왔습니다.", + "data": { + "data": [ + { + "identifier": "sirsoft-admin_basic", + "vendor": "sirsoft", + "name": "Admin Basic", + "version": "1.0.0", + "type": "admin", + "status": "uninstalled", + "description": "그누보드7 기본 관리자 템플릿", + "dependencies": { + "modules": [], + "plugins": [] + }, + "dependencies_met": true, + "update_available": false, + "update_source": null, + "latest_version": null, + "file_version": null, + "github_url": null, + "github_changelog_url": null, + "is_pending": false, + "is_bundled": false, + "deactivated_reason": null, + "deactivated_at": null, + "incompatible_required_version": null, + "abilities": { + "can_install": true, + "can_activate": true, + "can_uninstall": true, + "can_edit_layouts": true + } + }, + { + "identifier": "sirsoft-basic", + "vendor": "sirsoft", + "name": "Basic", + "version": "1.0.0", + "type": "user", + "status": "uninstalled", + "description": "그누보드7 기본 사용자 템플릿", + "dependencies": { + "modules": [ + { + "identifier": "sirsoft-board", + "name": "게시판", + "type": "module" + }, + { + "identifier": "sirsoft-ecommerce", + "name": "이커머스", + "type": "module" + }, + { + "identifier": "sirsoft-page", + "name": "페이지", + "type": "module" + } + ], + "plugins": [ + { + "identifier": "sirsoft-daum_postcode", + "name": "sirsoft-daum_postcode", + "type": "plugin" + } + ] + }, + "dependencies_met": false, + "update_available": false, + "update_source": null, + "latest_version": null, + "file_version": null, + "github_url": null, + "github_changelog_url": null, + "is_pending": false, + "is_bundled": false, + "deactivated_reason": null, + "deactivated_at": null, + "incompatible_required_version": null, + "abilities": { + "can_install": true, + "can_activate": true, + "can_uninstall": true, + "can_edit_layouts": true + } + } + ], + "pagination": { + "total": 2, + "current_page": 1, + "last_page": 1, + "per_page": 25 + }, + "meta": { + "total_templates": 2, + "active_templates": 0, + "admin_templates": 1, + "user_templates": 1, + "installed_templates": 2 + }, + "abilities": { + "can_install": true, + "can_activate": true, + "can_uninstall": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -93,10 +221,29 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.activate_validation_rules`). +**요청 예시** + +```http +POST /api/admin/templates/activate HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "template_name": "예시 이름", + "force": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -120,10 +267,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/admin/templates/check-updates HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -150,10 +310,28 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.deactivate_validation_rules`). +**요청 예시** + +```http +POST /api/admin/templates/deactivate HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "template_name": "예시 이름" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -183,10 +361,34 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.install_validation_rules`). +**요청 예시** + +```http +POST /api/admin/templates/install HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "template_name": "예시 이름", + "dependencies": [ + "예시값" + ], + "language_packs": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -214,10 +416,31 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.install_from_file_validation_rules`). +**요청 예시** + +```http +POST /api/admin/templates/install-from-file HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -245,10 +468,28 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.install_from_github_validation_rules`). +**요청 예시** + +```http +POST /api/admin/templates/install-from-github HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "github_url": "https://example.com" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -274,10 +515,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | attachment | path | string | 예 | — | 대상 attachment의 식별자 | +**요청 예시** + +```http +DELETE /api/admin/templates/layout-attachments/{attachment} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -303,10 +557,31 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | file | body | file | 예 | max 51200 | 업로드 파일 | +**요청 예시** + +```http +POST /api/admin/templates/manifest-preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -334,10 +609,28 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.refresh_layouts_validation_rules`). +**요청 예시** + +```http +POST /api/admin/templates/refresh-layouts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "template_name": "예시 이름" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -366,10 +659,23 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.uninstall_validation_rules`). +**요청 예시** + +```http +DELETE /api/admin/templates/uninstall?template_name=%EC%98%88%EC%8B%9C%20%EC%9D%B4%EB%A6%84&delete_data=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -400,10 +706,23 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.extension.changelog_rules`). +**요청 예시** + +```http +GET /api/admin/templates/{identifier}/changelog?source=active&from_version=%EC%98%88%EC%8B%9C%EA%B0%92&to_version=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -430,10 +749,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/templates/{identifier}/editor-assets HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -459,10 +791,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/templates/{identifier}/editor/broadcast-catalog.json HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -488,10 +833,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/templates/{identifier}/editor/components.css HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -517,10 +875,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/templates/{identifier}/editor/components.json HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -546,10 +917,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/templates/{identifier}/editor/editor-spec.json HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -576,10 +960,23 @@ _요청 파라미터 없음._ | identifier | path | string | 예 | — | 대상 리소스의 식별자 | | locale | path | string | 예 | — | 로케일 코드 (표시 언어/지역) | +**요청 예시** + +```http +GET /api/admin/templates/{identifier}/editor/lang/{locale}.json HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -605,10 +1002,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/templates/{identifier}/editor/permission-candidates.json HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -634,10 +1044,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/templates/{identifier}/editor/routes.json HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -672,10 +1095,40 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.seo_bot_preview.show_validation_rules`). +**요청 예시** + +```http +POST /api/admin/templates/{identifier}/editor/seo-bot-preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "layout": [ + "예시값" + ], + "route_params": [ + "예시값" + ], + "url": "https://example.com", + "locale": "ko", + "module_id": "예시값", + "plugin_id": "예시값", + "seed_context": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -706,10 +1159,23 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.seo_candidate.index_validation_rules`). +**요청 예시** + +```http +GET /api/admin/templates/{identifier}/editor/seo-candidates.json?extensions=%EC%98%88%EC%8B%9C%EA%B0%92&page_type=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -742,10 +1208,39 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.seo_og_preview.show_validation_rules`). +**요청 예시** + +```http +POST /api/admin/templates/{identifier}/editor/seo-og-preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "seo": [ + "예시값" + ], + "own_seo": [ + "예시값" + ], + "seed_context": [ + "예시값" + ], + "route_params": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -775,10 +1270,23 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template_layout_attachment.list_validation_rules`). +**요청 예시** + +```http +GET /api/admin/templates/{identifier}/layout-attachments?layout_name=%EC%98%88%EC%8B%9C%20%EC%9D%B4%EB%A6%84 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -809,10 +1317,35 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template_layout_attachment.upload_validation_rules`). +**요청 예시** + +```http +POST /api/admin/templates/{identifier}/layout-attachments HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary +Content-Disposition: form-data; name="layout_name" + +예시 이름 +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -839,10 +1372,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/admin/templates/{identifier}/license HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -868,10 +1414,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/templates/{templateName} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -897,10 +1456,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/templates/{templateName}/check-modified-layouts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -929,10 +1501,23 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.custom_translation.bulk_destroy_validation_rules`). +**요청 예시** + +```http +DELETE /api/admin/templates/{templateName}/custom-translations?ids=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -963,10 +1548,23 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.custom_translation.index_validation_rules`). +**요청 예시** + +```http +GET /api/admin/templates/{templateName}/custom-translations?layout_name=%EC%98%88%EC%8B%9C%20%EC%9D%B4%EB%A6%84&status=active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -998,10 +1596,30 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.custom_translation.store_validation_rules`). +**요청 예시** + +```http +POST /api/admin/templates/{templateName}/custom-translations HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "layout_name": "예시 이름", + "locale": "ko", + "value": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1029,10 +1647,23 @@ _요청 파라미터 없음._ | templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/admin/templates/{templateName}/custom-translations/{id} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1063,10 +1694,31 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.custom_translation.update_validation_rules`). +**요청 예시** + +```http +PUT /api/admin/templates/{templateName}/custom-translations/{id} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "values": [ + "예시값" + ], + "expected_lock_version": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1093,10 +1745,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/templates/{templateName}/install-preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1122,10 +1787,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/templates/{templateName}/layout-extensions HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1152,10 +1830,23 @@ _요청 파라미터 없음._ | templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | | extensionId | path | string | 예 | — | 대상 extension의 식별자 | +**요청 예시** + +```http +GET /api/admin/templates/{templateName}/layout-extensions/{extensionId} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1187,10 +1878,32 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.layout_extension.update_content_validation_rules`). +**요청 예시** + +```http +PUT /api/admin/templates/{templateName}/layout-extensions/{extensionId} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "expected_lock_version": 1, + "content": [ + "예시 내용입니다." + ], + "priority": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1222,10 +1935,31 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.layout_extension.store_preview_validation_rules`). +**요청 예시** + +```http +POST /api/admin/templates/{templateName}/layout-extensions/{extensionId}/preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "content": [ + "예시 내용입니다." + ], + "preview_layout": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1253,10 +1987,23 @@ _요청 파라미터 없음._ | templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | | extensionId | path | string | 예 | — | 대상 extension의 식별자 | +**요청 예시** + +```http +GET /api/admin/templates/{templateName}/layout-extensions/{extensionId}/versions HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1284,10 +2031,23 @@ _요청 파라미터 없음._ | extensionId | path | string | 예 | — | 대상 extension의 식별자 | | versionId | path | string | 예 | — | 대상 version의 식별자 | +**요청 예시** + +```http +POST /api/admin/templates/{templateName}/layout-extensions/{extensionId}/versions/{versionId}/restore HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1315,10 +2075,23 @@ _요청 파라미터 없음._ | extensionId | path | string | 예 | — | 대상 extension의 식별자 | | version | path | string | 예 | — | 대상 버전 (버전 문자열) | +**요청 예시** + +```http +GET /api/admin/templates/{templateName}/layout-extensions/{extensionId}/versions/{version} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1344,10 +2117,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/templates/{templateName}/layouts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1374,10 +2160,23 @@ _요청 파라미터 없음._ | templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | | name | path | string | 예 | — | 대상의 이름/명칭 | +**요청 예시** + +```http +GET /api/admin/templates/{templateName}/layouts/{name} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1408,10 +2207,31 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.layout.update_content_validation_rules`). +**요청 예시** + +```http +PUT /api/admin/templates/{templateName}/layouts/{name} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "expected_lock_version": 1, + "content": [ + "예시 내용입니다." + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1440,10 +2260,30 @@ _요청 파라미터 없음._ | name | path | string | 예 | — | 대상의 이름/명칭 | | content | body | array | 예 | — | 본문 내용 | +**요청 예시** + +```http +POST /api/admin/templates/{templateName}/layouts/{name}/preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "content": [ + "예시 내용입니다." + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1471,10 +2311,23 @@ _요청 파라미터 없음._ | templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | | name | path | string | 예 | — | 대상의 이름/명칭 | +**요청 예시** + +```http +GET /api/admin/templates/{templateName}/layouts/{name}/versions HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1502,10 +2355,23 @@ _요청 파라미터 없음._ | name | path | string | 예 | — | 대상의 이름/명칭 | | versionId | path | string | 예 | — | 대상 version의 식별자 | +**요청 예시** + +```http +POST /api/admin/templates/{templateName}/layouts/{name}/versions/{versionId}/restore HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1533,10 +2399,23 @@ _요청 파라미터 없음._ | name | path | string | 예 | — | 대상의 이름/명칭 | | version | path | string | 예 | — | 대상 버전 (버전 문자열) | +**요청 예시** + +```http +GET /api/admin/templates/{templateName}/layouts/{name}/versions/{version} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1562,10 +2441,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +**요청 예시** + +```http +GET /api/admin/templates/{templateName}/uninstall-info HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1595,10 +2487,29 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.perform_update_validation_rules`). +**요청 예시** + +```http +POST /api/admin/templates/{templateName}/update HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "layout_strategy": "overwrite", + "force": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1625,13 +2536,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | | path | path | string | 예 | — | 경로 | -| identifier | query | string | 예 | — | 대상 확장/리소스의 식별자 | -| path | query | string | 예 | — | 경로 | + +**요청 예시** + +```http +GET /api/templates/assets/{identifier}/{path} HTTP/1.1 +Host: api.example.com +Accept: application/json +``` **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1656,10 +2577,22 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/templates/{identifier}/components.json HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1683,10 +2616,22 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/templates/{identifier}/config.json HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1710,10 +2655,22 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/templates/{identifier}/editor-spec HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1738,10 +2695,22 @@ _요청 파라미터 없음._ | identifier | path | string | 예 | — | 대상 리소스의 식별자 | | locale | path | string | 예 | — | 로케일 코드 (표시 언어/지역) | +**요청 예시** + +```http +GET /api/templates/{identifier}/lang/{locale}.json HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1766,10 +2735,22 @@ _요청 파라미터 없음._ | identifier | path | string | 예 | — | 대상 리소스의 식별자 | | attachment | path | string | 예 | — | 대상 attachment의 식별자 | +**요청 예시** + +```http +GET /api/templates/{identifier}/layout-attachments/{attachment}/file HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1793,10 +2774,22 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/templates/{identifier}/routes.json HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/users.md b/docs/backend/api/users.md index bdc92c87..0a9afe28 100644 --- a/docs/backend/api/users.md +++ b/docs/backend/api/users.md @@ -38,6 +38,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.list_validation_rules`). +**요청 예시** + +```http +GET /api/admin/users?page=1&per_page=1&filters=%EC%98%88%EC%8B%9C%EA%B0%92&start_date=2026-01-01&end_date=2026-01-01&date_filter=all&sort_by=created_at&sort_order=asc HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -65,6 +74,112 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_read":true,"can_create":true,"can_update":true,"can…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "사용자 정보를 성공적으로 가져왔습니다.", + "data": { + "data": [ + { + "number": 2, + "uuid": "a234c3ea-a4f9-496d-a5e7-b44f0d53fc2f", + "name": "선지영", + "nickname": null, + "email": "jihee74@example.net", + "language": "ko", + "language_label": "한국어", + "country": null, + "country_flag": null, + "country_name": null, + "status": "active", + "status_label": "활성", + "status_variant": "success", + "mobile": null, + "roles": [], + "email_verified_at": "2026-07-08 10:44:49", + "last_login_at": null, + "created_at": "2026-07-08", + "is_owner": false, + "abilities": { + "can_read": true, + "can_create": true, + "can_update": true, + "can_delete": true, + "can_assign_roles": true + } + }, + { + "number": 1, + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "nickname": "song.hyunji", + "email": "apidoc-sample-user@example.com", + "language": "ko", + "language_label": "한국어", + "country": "KR", + "country_flag": "🇰🇷", + "country_name": "한국", + "status": "active", + "status_label": "활성", + "status_variant": "success", + "mobile": "010-9595-2897", + "roles": [ + { + "id": 1, + "identifier": "admin", + "name": "관리자" + }, + { + "id": 4, + "identifier": "apidoc-sample-role", + "name": "API 문서 샘플 역할" + } + ], + "email_verified_at": "2026-07-08 10:41:24", + "last_login_at": "2026-07-07 10:41:24", + "created_at": "2026-07-08", + "is_owner": true, + "abilities": { + "can_read": true, + "can_create": true, + "can_update": true, + "can_delete": true, + "can_assign_roles": true + } + } + ], + "statistics": { + "total_users": 2, + "users_this_week": 2, + "users_this_month": 2, + "users_today": 2, + "active_users_this_week": 1 + }, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true, + "can_assign_roles": true + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 2, + "from": 1, + "to": 2, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -120,10 +235,54 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.create_validation_rules`). +**요청 예시** + +```http +POST /api/admin/users HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": "예시 이름", + "nickname": "예시 이름", + "email": "user@example.com", + "password": "Password123!", + "language": "ko", + "country": "KR", + "timezone": "Asia/Seoul", + "status": "active", + "homepage": "https://example.com", + "mobile": "010-1234-5678", + "phone": "010-1234-5678", + "zipcode": "06234", + "address": "서울특별시 강남구 테헤란로 1", + "address_detail": "서울특별시 강남구 테헤란로 1", + "signature": "예시값", + "bio": "예시 내용입니다.", + "admin_memo": "예시값", + "roles": [ + "예시값" + ], + "role_ids": [ + "예시값" + ], + "notify_post_complete": true, + "notify_post_reply": true, + "notify_comment": true, + "notify_reply_comment": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -154,10 +313,31 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.bulk_update_status_validation_rules`). +**요청 예시** + +```http +PATCH /api/admin/users/bulk-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "status": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -188,10 +368,29 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.check_email_validation_rules`). +**요청 예시** + +```http +POST /api/admin/users/check-email HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "email": "user@example.com", + "exclude_user_id": "9f8b2c1a-4d3e-4a2b-8c1d-0e1f2a3b4c5d" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -221,9 +420,101 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.update_language_validation_rules`). +**요청 예시** + +```http +PATCH /api/admin/users/me/language HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "language": "ko" +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| uuid | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) | +| name | string | `API 문서 샘플 사용자` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| nickname | string | `song.hyunji` | 닉네임 | +| email | string | `apidoc-sample-user@example.com` | 이메일 주소 | +| avatar | null | `null` | 아바타 이미지 URL (미등록 시 null) | +| language | string | `ko` | 사용자 언어 설정 (ko: 한국어, en: 영어) | +| language_label | string | `한국어` | 언어 코드의 현지화 라벨 (user.language.{code} 번역) | +| country | string | `KR` | 국가 코드 (ISO 3166-1 alpha-2) | +| status | string | `active` | 상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조) | +| status_label | string | `활성` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| status_variant | string | `success` | 상태 표시 색상/스타일 변형 키 (상태 Enum variant() 산물 — UI 배지용) | +| is_admin | boolean | `true` | 관리자 역할 보유 여부 (User::isAdmin() — 역할 관계 기반 파생) | +| homepage | string | `https://example.com` | 홈페이지 URL | +| mobile | string | `010-9595-2897` | 휴대폰 번호 | +| phone | string | `02-637-5618` | 전화번호 | +| zipcode | string | `16505` | 우편번호 | +| address | string | `경기도 안양시 봉은사로 2918` | 기본 주소 | +| address_detail | string | `48동 718호` | 상세 주소 | +| signature | string | `Fugit consequuntur repellendus sed.` | 서명 | +| bio | string | `Ut magni et sunt ducimus error adipis…` | 자기소개 | +| last_login_at | string | `2026-07-07 10:41:24` | last login 일시 | +| email_verified_at | string | `2026-07-08 10:41:24` | email verified 일시 | +| timezone | string | `Asia/Seoul` | 사용자 시간대 (예: Asia/Seoul, UTC) | +| created_at | string | `2026-07-08 10:41:24` | 생성 일시 | +| updated_at | string | `2026-07-08 10:41:24` | 최종 수정 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_create":true,"can_update":true,"can…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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", + "avatar": null, + "language": "ko", + "language_label": "한국어", + "country": "KR", + "status": "active", + "status_label": "활성", + "status_variant": "success", + "is_admin": true, + "homepage": "https://example.com", + "mobile": "010-9595-2897", + "phone": "02-637-5618", + "zipcode": "16505", + "address": "경기도 안양시 봉은사로 2918", + "address_detail": "48동 718호", + "signature": "Fugit consequuntur repellendus sed.", + "bio": "Ut magni et sunt ducimus error adipisci. Pariatur corporis voluptatem ratione quo non saepe. Illo atque praesentium possimus dolores qui est fugit. Sint fugiat numquam voluptates.", + "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 + } + } +} +``` **에러 응답** @@ -249,6 +540,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/users/recent HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -281,6 +581,91 @@ _요청 파라미터 없음._ | is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_read":true,"can_create":true,"can_update":true,"can…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "사용자 정보를 성공적으로 가져왔습니다.", + "data": [ + { + "uuid": "a234c3ea-a4f9-496d-a5e7-b44f0d53fc2f", + "name": "선지영", + "nickname": null, + "email": "jihee74@example.net", + "avatar": null, + "language": "ko", + "language_label": "한국어", + "country": null, + "status": "active", + "status_label": "활성", + "status_variant": "success", + "is_admin": false, + "homepage": null, + "mobile": null, + "phone": null, + "zipcode": null, + "address": null, + "address_detail": null, + "signature": null, + "bio": null, + "last_login_at": null, + "email_verified_at": "2026-07-08 10:44:49", + "timezone": "Asia/Seoul", + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "is_owner": false, + "abilities": { + "can_read": true, + "can_create": true, + "can_update": true, + "can_delete": true, + "can_assign_roles": true + } + }, + { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "nickname": "song.hyunji", + "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": "https://example.com", + "mobile": "010-9595-2897", + "phone": "02-637-5618", + "zipcode": "16505", + "address": "경기도 안양시 봉은사로 2918", + "address_detail": "48동 718호", + "signature": "Fugit consequuntur repellendus sed.", + "bio": "Ut magni et sunt ducimus error adipisci. Pariatur corporis voluptatem ratione quo non saepe. Illo atque praesentium possimus dolores qui est fugit. Sint fugiat numquam voluptates.", + "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 + } + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -310,10 +695,23 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.search_validation_rules`). +**요청 예시** + +```http +GET /api/admin/users/search?keyword=%EC%98%88%EC%8B%9C%EA%B0%92&uuid=9f8b2c1a-4d3e-4a2b-8c1d-0e1f2a3b4c5d HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -339,6 +737,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/admin/users/statistics HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -352,6 +759,29 @@ _단건 응답: `data` 객체의 필드._ | active_users_this_week | integer | `2` | 이번 주 활동(로그인) 사용자 수 | | language_distribution | object | `{"ko":92,"en":64}` | 언어별 사용자 분포 (언어 코드 => 사용자 수) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "사용자 통계를 성공적으로 가져왔습니다.", + "data": { + "total_users": 2, + "users_this_week": 2, + "users_this_month": 2, + "users_today": 2, + "active_users_this_week": 1, + "language_distribution": { + "ko": 2 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -378,9 +808,34 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.delete_validation_rules`). +**요청 예시** + +```http +DELETE /api/admin/users/a234c2b1-cde8-437f-b28b-23323be2b98d HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "사용자가 성공적으로 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -410,6 +865,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | user | path | string | 예 | — | 대상 user의 식별자 | +**요청 예시** + +```http +GET /api/admin/users/a234c2b1-cde8-437f-b28b-23323be2b98d HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -489,6 +953,103 @@ _단건 응답: `data` 객체의 필드._ | ecommerce_preferred_shipping_country | null | `null` | 선호 배송 국가 코드 (sirsoft-ecommerce 모듈 주입, 미설정 시 null) | | ecommerce_preferred_shipping_country_name | null | `null` | 선호 배송 국가명 (배송 국가 코드에서 파생, sirsoft-ecommerce 모듈 주입) | +**응답 예시** + +```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", + "avatar": null, + "language": "ko", + "language_label": "한국어", + "country": "KR", + "status": "active", + "status_label": "활성", + "status_variant": "success", + "is_admin": true, + "homepage": "https://example.com", + "mobile": "010-9595-2897", + "phone": "02-637-5618", + "zipcode": "16505", + "address": "경기도 안양시 봉은사로 2918", + "address_detail": "48동 718호", + "signature": "Fugit consequuntur repellendus sed.", + "bio": "Ut magni et sunt ducimus error adipisci. Pariatur corporis voluptatem ratione quo non saepe. Illo atque praesentium possimus dolores qui est fugit. Sint fugiat numquam voluptates.", + "last_login_at": "2026-07-07 10:41:24", + "email_verified_at": "2026-07-08 10:41:24", + "timezone": "Asia/Seoul", + "modules_count": 0, + "plugins_count": 0, + "menus_count": 2, + "modules": [], + "plugins": [], + "menus": [ + { + "id": 1, + "title": "API 문서 샘플 메뉴", + "url": "/admin/apidoc-sample", + "is_active": true + }, + { + "id": 2, + "title": "하위 메뉴", + "url": "/admin/nihil-non-eos-doloribus-occaecati-optio", + "is_active": true + } + ], + "roles": [ + { + "id": 1, + "identifier": "admin", + "name": "관리자" + }, + { + "id": 4, + "identifier": "apidoc-sample-role", + "name": "API 문서 샘플 역할" + } + ], + "permissions": [], + "consents": [], + "terms_consent": null, + "privacy_consent": null, + "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 + }, + "admin_memo": "In officiis nemo quaerat debitis quia.", + "ip_address": "166.217.51.216", + "withdrawn_at": null, + "blocked_at": null, + "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 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -545,10 +1106,54 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.update_validation_rules`). +**요청 예시** + +```http +PUT /api/admin/users/a234c2b1-cde8-437f-b28b-23323be2b98d HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": "예시 이름", + "nickname": "예시 이름", + "email": "user@example.com", + "password": "Password123!", + "language": "ko", + "country": "KR", + "timezone": "Asia/Seoul", + "status": "active", + "homepage": "https://example.com", + "mobile": "010-1234-5678", + "phone": "010-1234-5678", + "zipcode": "06234", + "address": "서울특별시 강남구 테헤란로 1", + "address_detail": "서울특별시 강남구 테헤란로 1", + "signature": "예시값", + "bio": "예시 내용입니다.", + "admin_memo": "예시값", + "roles": [ + "예시값" + ], + "role_ids": [ + "예시값" + ], + "notify_post_complete": true, + "notify_post_reply": true, + "notify_comment": true, + "notify_reply_comment": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -577,6 +1182,14 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | user | path | string | 예 | — | 대상 user의 식별자 | +**요청 예시** + +```http +GET /api/users/a234c2b1-cde8-437f-b28b-23323be2b98d/profile HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -592,6 +1205,29 @@ _단건 응답: `data` 객체의 필드._ | created_at | string | `2026-07-06` | 생성 일시 | | is_withdrawn | boolean | `false` | withdrawn 여부 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "프로필 정보를 성공적으로 가져왔습니다.", + "data": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "status": "active", + "status_label": "활성", + "avatar": null, + "bio": "Ut magni et sunt ducimus error adipisci. Pariatur corporis voluptatem ratione quo non saepe. Illo atque praesentium possimus dolores qui est fugit. Sint fugiat numquam voluptates.", + "created_at": "2026-07-08", + "is_withdrawn": false + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/backend/api/verify-password.md b/docs/backend/api/verify-password.md index 35a88b04..44d418dd 100644 --- a/docs/backend/api/verify-password.md +++ b/docs/backend/api/verify-password.md @@ -31,10 +31,28 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.verify_password_rules`). +**요청 예시** + +```http +POST /api/me/verify-password HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "password": "Password123!" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/docs/cheatsheet.md b/docs/cheatsheet.md index 621254f3..905f9d48 100644 --- a/docs/cheatsheet.md +++ b/docs/cheatsheet.md @@ -253,6 +253,22 @@ php artisan seo:generate-sitemap # Sitemap 생성 (큐 디스패치) php artisan seo:generate-sitemap --sync # Sitemap 동기 생성 ``` +### API 문서 Artisan 커맨드 + +```bash +# API 레퍼런스 문서 생성/갱신 (실측 기반 스캐폴딩). 상세: docs/backend/api-documentation.md +php artisan api:docgen --scope=core # 코어 문서 생성 (범위: core|module:{id}|plugin:{id}|all) +php artisan api:docgen --scope=core --seed # 실측용 완전 샘플 시드 후 생성 (개발 환경 전용) +php artisan api:docgen --scope=core --base-url=https://example.com # 실측 기준 URL 지정 +php artisan api:docgen --scope=core --examples-only # 표·서술 불가침, 요청/응답 예시 블록만 in-place 삽입 +php artisan api:docgen --scope=core --check # 생성 없이 누락/drift 만 리포트 (하네스 소비) +php artisan api:docgen --scope=core --dry-run # 생성 대상 파일/엔드포인트 목록만 출력 + +# 파라미터/응답 필드 설명 TODO 셀 in-place 백필 (재생성 없이 공통 필드 자동 서술, 멱등) +php artisan api:docgen-backfill-params # 요청 파라미터 표 TODO 셀 백필 +php artisan api:docgen-backfill-fields # 응답 필드 표 TODO 셀 백필 +``` + --- ## 확장 업데이트 (CLI + API) diff --git a/docs/extension/module-routing.md b/docs/extension/module-routing.md index 3a6bfd08..fe763e57 100644 --- a/docs/extension/module-routing.md +++ b/docs/extension/module-routing.md @@ -7,8 +7,8 @@ ## TL;DR (5초 요약) ```text -1. URL prefix 자동: /api/admin/[vendor-module]/... -2. name prefix 자동: api.[vendor-module].... +1. URL prefix 자동: /api/modules/[vendor-module]/... +2. name prefix 자동: api.modules.[vendor-module].... 3. 활성화된 모듈만 라우트 등록됨 4. 프론트엔드: routes/admin.json, routes/user.json으로 admin/user 분기 5. 레거시 routes.json은 admin으로 폴백 (경고 로그 출력) diff --git a/modules/_bundled/gnuboard7-hello_module/docs/api/memos.md b/modules/_bundled/gnuboard7-hello_module/docs/api/memos.md index 821b1e66..286c241d 100644 --- a/modules/_bundled/gnuboard7-hello_module/docs/api/memos.md +++ b/modules/_bundled/gnuboard7-hello_module/docs/api/memos.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/gnuboard7-hello_module/memos HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) @@ -74,6 +83,15 @@ _대표 에러 없음 (공개 조회). diff --git a/modules/_bundled/sirsoft-board/docs/api/activity-stats.md b/modules/_bundled/sirsoft-board/docs/api/activity-stats.md index 348cac91..4a44abae 100644 --- a/modules/_bundled/sirsoft-board/docs/api/activity-stats.md +++ b/modules/_bundled/sirsoft-board/docs/api/activity-stats.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-board/me/activity-stats HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -37,6 +46,24 @@ _단건 응답: `data` 객체의 필드._ | total_comments | integer | `0` | 회원 본인이 작성한 게시글들의 댓글 수 합계(SUM of comments_count)입니다. 비활성 게시판 글은 제외됩니다. | | total_views | integer | `42` | 회원 본인이 작성한 게시글들의 누적 조회수 합계(SUM of view_count)입니다. 비활성 게시판 글은 제외됩니다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "활동 통계를 조회했습니다.", + "data": { + "total_posts": 1, + "total_comments": 0, + "total_views": 44 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-board/docs/api/board-activities.md b/modules/_bundled/sirsoft-board/docs/api/board-activities.md index 0bbc4cc9..0cd00961 100644 --- a/modules/_bundled/sirsoft-board/docs/api/board-activities.md +++ b/modules/_bundled/sirsoft-board/docs/api/board-activities.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-board/me/board-activities HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -47,6 +56,75 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | | content_plain | string | `API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.` | 게시글 본문의 순수 텍스트입니다. HTML 모드 글은 태그를 제거한 평문으로 변환되며, 목록 미리보기용으로 사용합니다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "내 활동 게시글을 조회했습니다.", + "data": { + "current_page": 1, + "data": [ + { + "id": 1, + "board_slug": "apidoc-sample-board", + "board_name": "API 문서 샘플 게시판", + "activity_type": "authored", + "activity_count": 0, + "title": "API 문서 샘플 게시글", + "is_secret": "{MASKED}", + "status": "published", + "view_count": 44, + "comment_count": 0, + "created_at": "2026-07-08 10:41:34", + "created_at_formatted": "4시간 전", + "content_plain": "API 레퍼런스 실측용 완전 샘플 게시글 본문입니다." + } + ], + "first_page_url": "https://api.example.com/api/modules/sirsoft-board/me/board-activities?page=1", + "from": 1, + "last_page": 1, + "last_page_url": "https://api.example.com/api/modules/sirsoft-board/me/board-activities?page=1", + "links": [ + { + "url": null, + "label": "pagination.previous", + "page": null, + "active": false + }, + { + "url": "https://api.example.com/api/modules/sirsoft-board/me/board-activities?page=1", + "label": "1", + "page": 1, + "active": true + }, + { + "url": null, + "label": "pagination.next", + "page": null, + "active": false + } + ], + "next_page_url": null, + "path": "https://api.example.com/api/modules/sirsoft-board/me/board-activities", + "per_page": 25, + "prev_page_url": null, + "to": 1, + "total": 1, + "query": { + "board_slug": "", + "search": "", + "sort": "latest", + "activity_type": "" + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-board/docs/api/board-types.md b/modules/_bundled/sirsoft-board/docs/api/board-types.md index f3393e2d..8245bf2c 100644 --- a/modules/_bundled/sirsoft-board/docs/api/board-types.md +++ b/modules/_bundled/sirsoft-board/docs/api/board-types.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/board-types HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -35,6 +44,45 @@ _요청 파라미터 없음._ | slug | string | `basic` | 유형 식별자 (basic, card, gallery 등) | | name | object | `{"ko":"기본형","en":"Basic List","ja":"基本形"}` | 유형명 (다국어: {"ko": "기본형", "en": "Basic List"}) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시판 유형 목록을 조회했습니다.", + "data": [ + { + "id": 2, + "slug": "basic", + "name": { + "ko": "기본형", + "en": "Basic List" + } + }, + { + "id": 3, + "slug": "gallery", + "name": { + "ko": "갤러리형", + "en": "Gallery" + } + }, + { + "id": 4, + "slug": "card", + "name": { + "ko": "카드형", + "en": "Card" + } + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -60,10 +108,29 @@ _요청 파라미터 없음._ | slug | body | string | 예 | max 50 | URL 친화 식별자 (slug) | | name | body | string | 예 | — | 대상의 이름/명칭 | +**요청 예시** + +```http +POST /api/modules/sirsoft-board/admin/board-types HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "slug": "example-key", + "name": "예시 이름" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -89,10 +156,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-board/admin/board-types/2 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -119,10 +199,28 @@ _요청 파라미터 없음._ | id | path | string | 예 | — | 대상 리소스의 식별자 | | name | body | string | 예 | — | 대상의 이름/명칭 | +**요청 예시** + +```http +PUT /api/modules/sirsoft-board/admin/board-types/2 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": "예시 이름" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-board/docs/api/board.md b/modules/_bundled/sirsoft-board/docs/api/board.md index 4ed6fcb6..bf41d182 100644 --- a/modules/_bundled/sirsoft-board/docs/api/board.md +++ b/modules/_bundled/sirsoft-board/docs/api/board.md @@ -35,10 +35,43 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.attachment.upload_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-board/admin/board/apidoc-sample-board/attachments HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary +Content-Disposition: form-data; name="post_id" + +1 +------G7ExampleBoundary +Content-Disposition: form-data; name="collection" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="temp_key" + +예시값 +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -66,10 +99,23 @@ | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/board/apidoc-sample-board/attachments/download/apidocsmpl1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -98,10 +144,30 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.attachment.reorder_validation_rules`). +**요청 예시** + +```http +PATCH /api/modules/sirsoft-board/admin/board/apidoc-sample-board/attachments/reorder HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "order": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -129,9 +195,34 @@ | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-board/admin/board/apidoc-sample-board/attachments/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "파일이 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -158,6 +249,15 @@ | --- | --- | --- | --- | --- | --- | | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -194,6 +294,101 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | number | integer | `1` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | | show_category | boolean | `false` | 목록에 카테고리 열을 노출할지 여부. 게시판 설정(`show_category`)에서 파생되어 각 행에 부여됩니다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시글 목록을 조회했습니다.", + "data": { + "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": "{MASKED}", + "content_mode": "html", + "is_new": true, + "status": "published", + "status_label": "게시됨", + "view_count": 43, + "comment_count": 0, + "reply_count": 0, + "attachment_count": 0, + "has_attachment": false, + "thumbnail": "/api/modules/sirsoft-board/boards/apidoc-sample-board/attachment/apidocsmpl1/preview", + "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, + "slug": "apidoc-sample-board", + "title": "API 문서 샘플 게시글", + "deleted_at": null, + "content_preview": "API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.", + "row_type": "normal", + "number": 1, + "show_category": false + } + ], + "pagination": { + "total": 1, + "all_total": 1, + "count": 1, + "per_page": 25, + "current_page": 1, + "last_page": 1, + "from": 1, + "to": 1, + "has_more_pages": false + }, + "board": { + "slug": "apidoc-sample-board", + "name": "API 문서 샘플 게시판", + "type": "basic", + "categories": [], + "show_category": false, + "settings": { + "use_file_upload": true, + "use_comment": true, + "use_reply": true, + "use_report": true, + "secret_mode": "{MASKED}", + "per_page": 20, + "per_page_mobile": 15, + "order_by": "created_at", + "order_direction": "DESC" + } + }, + "abilities": { + "can_read": true, + "can_write": true, + "can_read_secret": "{MASKED}", + "can_read_comments": true, + "can_write_comments": true, + "can_upload": true, + "can_download": true, + "can_manage": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -221,10 +416,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.post.store_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -251,6 +459,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts/form-data HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -265,6 +482,28 @@ _단건 응답: `data` 객체의 필드._ | is_secret | boolean | `false` | secret 여부 | | parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시글 폼 데이터를 조회했습니다.", + "data": { + "title": "", + "content": "", + "content_mode": "text", + "category": null, + "is_notice": false, + "is_secret": "{MASKED}", + "parent_id": null + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -290,6 +529,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts/form-meta HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -298,6 +546,88 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | board | object | `{"id":12,"name":"API 문서 샘플 게시판","slug":"apidoc-sample-boa…` | 폼 화면 표시에 필요한 게시판 정보 객체(이름·슬러그·댓글/답글/비밀글 설정 등). 사용자 권한(abilities)과 함께 항상 포함됩니다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시글 폼 메타 데이터를 조회했습니다.", + "data": { + "board": { + "id": 1, + "name": "API 문서 샘플 게시판", + "slug": "apidoc-sample-board", + "is_active": true, + "type": "basic", + "description": "", + "per_page": 20, + "per_page_mobile": 15, + "order_by": "created_at", + "order_direction": "DESC", + "categories": [], + "show_view_count": true, + "secret_mode": "{MASKED}", + "use_comment": true, + "use_reply": true, + "max_reply_depth": 5, + "use_report": true, + "comment_order": "ASC", + "max_comment_depth": 10, + "min_title_length": 2, + "max_title_length": 200, + "min_content_length": 10, + "max_content_length": 10000, + "min_comment_length": 2, + "max_comment_length": 1000, + "blocked_keywords": [], + "use_file_upload": true, + "max_file_size": 10, + "max_file_count": 5, + "allowed_extensions": [ + "jpg", + "jpeg", + "png", + "gif", + "pdf", + "zip" + ], + "add_to_menu": null, + "new_display_hours": 24, + "board_managers": [], + "board_steps": [], + "board_manager_ids": [], + "board_step_ids": [], + "notify_author": true, + "notify_admin_on_post": true, + "created_at": "2026-07-08 10:41:34", + "updated_at": "2026-07-08 10:41:34", + "permissions": null, + "category_post_counts": null, + "posts_count": 0, + "user_abilities": { + "can_read": true, + "can_write": true, + "can_read_secret": "{MASKED}", + "can_read_comments": true, + "can_write_comments": true, + "can_upload": true, + "can_download": true, + "can_manage": true + }, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -324,9 +654,138 @@ _단건 응답: `data` 객체의 필드._ | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| category | null | `null` | 게시글 분류(카테고리) 문자열. 게시판이 카테고리를 쓰지 않거나 미지정 시 null (최대 50자). | +| author | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 작성자 사용자 객체 (uuid/name — author 관계 파생) | +| is_notice | boolean | `false` | notice 여부 | +| is_secret | boolean | `false` | secret 여부 | +| content_mode | string | `html` | 본문 편집 모드. `html`(위지윅/HTML) 또는 `text`(평문)이며, 요약·썸네일 추출과 렌더링 방식을 결정합니다. 미지정 시 `text`. | +| is_new | boolean | `true` | new 여부 | +| status | string | `deleted` | 상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조) | +| status_label | string | `삭제됨` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| view_count | integer | `43` | view 개수 (집계) | +| comment_count | integer | `0` | comment 개수 (집계) | +| reply_count | integer | `0` | reply 개수 (집계) | +| attachment_count | integer | `0` | attachment 개수 (집계) | +| has_attachment | boolean | `false` | attachment 여부 | +| thumbnail | null | `null` | 썸네일 이미지 URL/경로 | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| is_reply | boolean | `false` | reply 여부 | +| created_at | string | `2026-07-08 10:41:34` | 생성 일시 | +| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| is_author | boolean | `true` | author 여부 | +| is_guest_post | boolean | `false` | guest post 여부 | +| title | string | `API 문서 샘플 게시글` | 제목 | +| content | string | `

API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.

` | 본문 내용 | +| user_id | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | user 식별자 (연관 리소스 참조) | +| trigger_type | string | `admin` | 동작을 유발한 방식/주체 구분 값 | +| updated_at | string | `2026-07-08 15:01:43` | 최종 수정 일시 | +| deleted_at | string | `2026-07-08 15:01:43` | 소프트 삭제 일시 (미삭제 시 null) | +| ip_address | string | `127.0.0.1` | 요청/행위가 발생한 IP 주소 | +| action_logs | array | `[{"action":"delete","reason":null,"admin_name":"API 문서 샘플…` | 블라인드/복원/삭제 등 처리 이력 목록(항목별 action·reason·admin_name·created_at). `admin.manage` 권한 보유자에게만 노출되며, 민감 필드(admin_id·ip_address)는 제외됩니다. 비권한자에게는 null. | +| board | null | `null` | 소속 게시판 정보 객체(슬러그·이름·유형·댓글/답글/신고 사용 여부·조회수 표시·최대 답글/댓글 깊이·신고 사유 목록). board 관계가 로드된 경우에만 채워지며, 아니면 null. | +| navigation | null | `null` | 이전/다음 게시글 이동 정보(`prev`·`next`). 상세 로드 시 계산되며, 쓰기 응답처럼 인접 게시글을 계산하지 않은 경우 null. | +| parent | null | `null` | 상위 항목 객체 (parent 관계 파생) | +| comments | null | `null` | 게시글에 달린 댓글 목록(CommentResource 컬렉션). comments 관계가 로드된 경우에만 채워지며, 아니면 null. | +| attachments | null | `null` | 게시글 첨부파일 목록(AttachmentResource 컬렉션). attachments 관계가 로드된 경우에만 채워지며, 아니면 null(비밀글·삭제글은 권한에 따라 빈 배열 또는 연쇄 삭제분만 노출). | +| replies | null | `null` | 이 게시글에 달린 답변글 목록(PostResource 컬렉션, 재귀). replies 관계가 로드된 경우에만 채워지며, 아니면 null. | +| is_already_reported | boolean | `false` | already reported 여부 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_write":true,"can_read_secret":true,…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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": "{MASKED}", + "content_mode": "html", + "is_new": true, + "status": "deleted", + "status_label": "삭제됨", + "view_count": 43, + "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": "

API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.

", + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "trigger_type": "admin", + "updated_at": "2026-07-08 15:01:43", + "deleted_at": "2026-07-08 15:01:43", + "ip_address": "127.0.0.1", + "action_logs": [ + { + "action": "delete", + "reason": null, + "admin_name": "API 문서 샘플 사용자", + "created_at": "2026-07-08 06:01:43" + } + ], + "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": "{MASKED}", + "can_read_comments": true, + "can_write_comments": true, + "can_upload": true, + "can_download": true, + "can_manage": true + } + } +} +``` **에러 응답** @@ -354,6 +813,15 @@ _단건 응답: `data` 객체의 필드._ | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -400,6 +868,180 @@ _단건 응답: `data` 객체의 필드._ | is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_read":true,"can_write":true,"can_read_secret":true,…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```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": "{MASKED}", + "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": "/api/modules/sirsoft-board/boards/apidoc-sample-board/attachment/apidocsmpl1/preview", + "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": "

API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.

", + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "trigger_type": "user", + "updated_at": "2026-07-08 15:01:44", + "deleted_at": null, + "ip_address": "127.0.0.1", + "action_logs": [], + "board": { + "slug": "apidoc-sample-board", + "name": "API 문서 샘플 게시판", + "type": "basic", + "use_comment": true, + "use_reply": true, + "use_report": true, + "show_view_count": true, + "max_reply_depth": 5, + "max_comment_depth": 10, + "report_types": [ + { + "value": "abuse", + "label": "욕설/비방" + }, + { + "value": "hate_speech", + "label": "혐오 발언" + }, + { + "value": "spam", + "label": "스팸/광고" + }, + { + "value": "copyright", + "label": "저작권 침해" + }, + { + "value": "privacy", + "label": "개인정보 노출" + }, + { + "value": "misinformation", + "label": "허위정보" + }, + { + "value": "sexual", + "label": "성적인 콘텐츠" + }, + { + "value": "violence", + "label": "폭력적인 콘텐츠" + }, + { + "value": "other", + "label": "기타" + } + ] + }, + "navigation": { + "prev": null, + "next": null + }, + "parent": null, + "comments": [ + { + "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": "{MASKED}", + "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 10:41:34", + "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 + } + } + ], + "attachments": [ + { + "id": 1, + "hash": "apidocsmpl1", + "original_filename": "apidoc-sample.png", + "mime_type": "image/png", + "size": 2048, + "size_formatted": "2 KB", + "collection": "default", + "order": 0, + "download_url": "/api/modules/sirsoft-board/boards/apidoc-sample-board/attachment/apidocsmpl1", + "preview_url": "/api/modules/sirsoft-board/boards/apidoc-sample-board/attachment/apidocsmpl1/preview", + "is_image": true, + "meta": null + } + ], + "replies": [], + "is_already_reported": false, + "is_owner": true, + "abilities": { + "can_read": true, + "can_write": true, + "can_read_secret": "{MASKED}", + "can_read_comments": true, + "can_write_comments": true, + "can_upload": true, + "can_download": true, + "can_manage": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -428,9 +1070,131 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.post.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| category | null | `null` | 게시글 분류(카테고리) 문자열. 게시판이 카테고리를 쓰지 않거나 미지정 시 null (최대 50자). | +| author | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 작성자 사용자 객체 (uuid/name — author 관계 파생) | +| is_notice | boolean | `false` | notice 여부 | +| is_secret | boolean | `false` | secret 여부 | +| content_mode | string | `html` | 본문 편집 모드. `html`(위지윅/HTML) 또는 `text`(평문)이며, 요약·썸네일 추출과 렌더링 방식을 결정합니다. 미지정 시 `text`. | +| is_new | boolean | `true` | new 여부 | +| status | string | `published` | 상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조) | +| status_label | string | `게시됨` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| view_count | integer | `44` | view 개수 (집계) | +| comment_count | integer | `0` | comment 개수 (집계) | +| reply_count | integer | `0` | reply 개수 (집계) | +| attachment_count | integer | `0` | attachment 개수 (집계) | +| has_attachment | boolean | `false` | attachment 여부 | +| thumbnail | null | `null` | 썸네일 이미지 URL/경로 | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| is_reply | boolean | `false` | reply 여부 | +| created_at | string | `2026-07-08 10:41:34` | 생성 일시 | +| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| is_author | boolean | `true` | author 여부 | +| is_guest_post | boolean | `false` | guest post 여부 | +| title | string | `API 문서 샘플 게시글` | 제목 | +| content | string | `

API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.

` | 본문 내용 | +| user_id | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | user 식별자 (연관 리소스 참조) | +| trigger_type | string | `user` | 동작을 유발한 방식/주체 구분 값 | +| updated_at | string | `2026-07-08 15:01:44` | 최종 수정 일시 | +| deleted_at | null | `null` | 소프트 삭제 일시 (미삭제 시 null) | +| ip_address | string | `127.0.0.1` | 요청/행위가 발생한 IP 주소 | +| action_logs | array | `[]` | 블라인드/복원/삭제 등 처리 이력 목록(항목별 action·reason·admin_name·created_at). `admin.manage` 권한 보유자에게만 노출되며, 민감 필드(admin_id·ip_address)는 제외됩니다. 비권한자에게는 null. | +| board | null | `null` | 소속 게시판 정보 객체(슬러그·이름·유형·댓글/답글/신고 사용 여부·조회수 표시·최대 답글/댓글 깊이·신고 사유 목록). board 관계가 로드된 경우에만 채워지며, 아니면 null. | +| navigation | null | `null` | 이전/다음 게시글 이동 정보(`prev`·`next`). 상세 로드 시 계산되며, 쓰기 응답처럼 인접 게시글을 계산하지 않은 경우 null. | +| parent | null | `null` | 상위 항목 객체 (parent 관계 파생) | +| comments | null | `null` | 게시글에 달린 댓글 목록(CommentResource 컬렉션). comments 관계가 로드된 경우에만 채워지며, 아니면 null. | +| attachments | null | `null` | 게시글 첨부파일 목록(AttachmentResource 컬렉션). attachments 관계가 로드된 경우에만 채워지며, 아니면 null(비밀글·삭제글은 권한에 따라 빈 배열 또는 연쇄 삭제분만 노출). | +| replies | null | `null` | 이 게시글에 달린 답변글 목록(PostResource 컬렉션, 재귀). replies 관계가 로드된 경우에만 채워지며, 아니면 null. | +| is_already_reported | boolean | `false` | already reported 여부 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_write":true,"can_read_secret":true,…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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": "{MASKED}", + "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": "

API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.

", + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "trigger_type": "user", + "updated_at": "2026-07-08 15:01:44", + "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": "{MASKED}", + "can_read_comments": true, + "can_write_comments": true, + "can_upload": true, + "can_download": true, + "can_manage": true + } + } +} +``` **에러 응답** @@ -460,9 +1224,143 @@ _단건 응답: `data` 객체의 필드._ | id | path | string | 예 | — | 대상 리소스의 식별자 | | reason | body | string | 아니오 | max 1000 | 블라인드 처리 사유(최대 1000자). 처리 이력(action_logs)에 기록되며, 미지정 시 빈 문자열로 저장됩니다. | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts/1/blind HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "reason": "예시값" +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| category | null | `null` | 게시글 분류(카테고리) 문자열. 게시판이 카테고리를 쓰지 않거나 미지정 시 null (최대 50자). | +| author | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 작성자 사용자 객체 (uuid/name — author 관계 파생) | +| is_notice | boolean | `false` | notice 여부 | +| is_secret | boolean | `false` | secret 여부 | +| content_mode | string | `html` | 본문 편집 모드. `html`(위지윅/HTML) 또는 `text`(평문)이며, 요약·썸네일 추출과 렌더링 방식을 결정합니다. 미지정 시 `text`. | +| is_new | boolean | `true` | new 여부 | +| status | string | `blinded` | 상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조) | +| status_label | string | `블라인드` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| view_count | integer | `44` | view 개수 (집계) | +| comment_count | integer | `0` | comment 개수 (집계) | +| reply_count | integer | `0` | reply 개수 (집계) | +| attachment_count | integer | `0` | attachment 개수 (집계) | +| has_attachment | boolean | `false` | attachment 여부 | +| thumbnail | null | `null` | 썸네일 이미지 URL/경로 | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| is_reply | boolean | `false` | reply 여부 | +| created_at | string | `2026-07-08 10:41:34` | 생성 일시 | +| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| is_author | boolean | `true` | author 여부 | +| is_guest_post | boolean | `false` | guest post 여부 | +| title | string | `API 문서 샘플 게시글` | 제목 | +| content | string | `

API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.

` | 본문 내용 | +| user_id | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | user 식별자 (연관 리소스 참조) | +| trigger_type | string | `user` | 동작을 유발한 방식/주체 구분 값 | +| updated_at | string | `2026-07-08 15:01:44` | 최종 수정 일시 | +| deleted_at | null | `null` | 소프트 삭제 일시 (미삭제 시 null) | +| ip_address | string | `127.0.0.1` | 요청/행위가 발생한 IP 주소 | +| action_logs | array | `[{"action":"blind","reason":"실측 예시값","admin_name":"API 문서…` | 블라인드/복원/삭제 등 처리 이력 목록(항목별 action·reason·admin_name·created_at). `admin.manage` 권한 보유자에게만 노출되며, 민감 필드(admin_id·ip_address)는 제외됩니다. 비권한자에게는 null. | +| board | null | `null` | 소속 게시판 정보 객체(슬러그·이름·유형·댓글/답글/신고 사용 여부·조회수 표시·최대 답글/댓글 깊이·신고 사유 목록). board 관계가 로드된 경우에만 채워지며, 아니면 null. | +| navigation | null | `null` | 이전/다음 게시글 이동 정보(`prev`·`next`). 상세 로드 시 계산되며, 쓰기 응답처럼 인접 게시글을 계산하지 않은 경우 null. | +| parent | null | `null` | 상위 항목 객체 (parent 관계 파생) | +| comments | null | `null` | 게시글에 달린 댓글 목록(CommentResource 컬렉션). comments 관계가 로드된 경우에만 채워지며, 아니면 null. | +| attachments | null | `null` | 게시글 첨부파일 목록(AttachmentResource 컬렉션). attachments 관계가 로드된 경우에만 채워지며, 아니면 null(비밀글·삭제글은 권한에 따라 빈 배열 또는 연쇄 삭제분만 노출). | +| replies | null | `null` | 이 게시글에 달린 답변글 목록(PostResource 컬렉션, 재귀). replies 관계가 로드된 경우에만 채워지며, 아니면 null. | +| is_already_reported | boolean | `false` | already reported 여부 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_write":true,"can_read_secret":true,…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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": "{MASKED}", + "content_mode": "html", + "is_new": true, + "status": "blinded", + "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": "

API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.

", + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "trigger_type": "user", + "updated_at": "2026-07-08 15:01:44", + "deleted_at": null, + "ip_address": "127.0.0.1", + "action_logs": [ + { + "action": "blind", + "reason": "실측 예시값", + "admin_name": "API 문서 샘플 사용자", + "created_at": "2026-07-08 06:01:44" + } + ], + "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": "{MASKED}", + "can_read_comments": true, + "can_write_comments": true, + "can_upload": true, + "can_download": true, + "can_manage": true + } + } +} +``` **에러 응답** @@ -492,10 +1390,28 @@ _단건 응답: `data` 객체의 필드._ | id | path | string | 예 | — | 대상 리소스의 식별자 | | reason | body | string | 아니오 | max 1000 | 블라인드 복원 사유(최대 1000자). 처리 이력(action_logs)에 기록되며, 미지정 시 null로 전달됩니다. | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts/1/restore HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "reason": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -525,10 +1441,23 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.comment.store_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts/1/comments HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -557,9 +1486,34 @@ _단건 응답: `data` 객체의 필드._ | postId | path | string | 예 | — | 대상 post의 식별자 | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts/1/comments/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "댓글이 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -590,10 +1544,23 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.comment.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts/1/comments/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -623,9 +1590,104 @@ _단건 응답: `data` 객체의 필드._ | id | path | string | 예 | — | 대상 리소스의 식별자 | | reason | body | string | 아니오 | max 1000 | 댓글 블라인드 처리 사유(최대 1000자). 처리 이력에 기록되며, 미지정 시 빈 문자열로 저장됩니다. | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts/1/comments/1/blind HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "reason": "예시값" +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| post_id | integer | `1` | post 식별자 (연관 리소스 참조) | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| content | string | `API 문서 샘플 댓글입니다.` | 본문 내용 | +| author | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 작성자 사용자 객체 (uuid/name — author 관계 파생) | +| is_secret | boolean | `false` | secret 여부 | +| status | string | `blinded` | 상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조) | +| status_label | string | `블라인드` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| replies_count | integer | `0` | replies 개수 (집계) | +| created_at | string | `2026-07-08 10:41:34` | 생성 일시 | +| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| updated_at | string | `2026-07-08 15:01:44` | 최종 수정 일시 | +| deleted_at | null | `null` | 소프트 삭제 일시 (미삭제 시 null) | +| is_cascade_deleted | boolean | `false` | cascade deleted 여부 | +| ip_address | null | `null` | 요청/행위가 발생한 IP 주소 | +| action_logs | array | `[{"action":"blind","reason":"실측 예시값","admin_name":"API 문서…` | 블라인드/복원/삭제 등 처리 이력 목록(항목별 action·reason·admin_name·created_at). `admin.manage` 권한 보유자에게만 노출되며, 민감 필드(admin_id·ip_address)는 제외됩니다. 비권한자에게는 null. | +| is_author | boolean | `true` | author 여부 | +| is_guest_comment | boolean | `false` | guest comment 여부 | +| is_already_reported | boolean | `false` | already reported 여부 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_write":true,"can_manage":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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": "{MASKED}", + "status": "blinded", + "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:44", + "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" + } + ], + "is_author": true, + "is_guest_comment": false, + "is_already_reported": false, + "is_owner": true, + "abilities": { + "can_read": true, + "can_write": true, + "can_manage": true + } + } +} +``` **에러 응답** @@ -655,10 +1717,23 @@ _단건 응답: `data` 객체의 필드._ | postId | path | string | 예 | — | 대상 post의 식별자 | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-board/admin/board/apidoc-sample-board/posts/1/comments/1/restore HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-board/docs/api/boards.md b/modules/_bundled/sirsoft-board/docs/api/boards.md index 2685d377..b9b01405 100644 --- a/modules/_bundled/sirsoft-board/docs/api/boards.md +++ b/modules/_bundled/sirsoft-board/docs/api/boards.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/boards HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -79,6 +88,95 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | user_abilities | null | `null` | 현재 사용자의 게시판별 세부 권한 맵 (can_read/can_write/can_read_secret/can_manage 등 — include_user_abilities 요청 시에만 채워지며 미포함 시 null) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시판 정보를 조회했습니다.", + "data": { + "data": [ + { + "id": 1, + "name": "API 문서 샘플 게시판", + "slug": "apidoc-sample-board", + "is_active": true, + "type": "basic", + "description": "", + "per_page": 20, + "per_page_mobile": 15, + "order_by": "created_at", + "order_direction": "DESC", + "categories": [], + "show_view_count": true, + "secret_mode": "{MASKED}", + "use_comment": true, + "use_reply": true, + "max_reply_depth": 5, + "use_report": true, + "comment_order": "ASC", + "max_comment_depth": 10, + "min_title_length": 2, + "max_title_length": 200, + "min_content_length": 10, + "max_content_length": 10000, + "min_comment_length": 2, + "max_comment_length": 1000, + "blocked_keywords": [], + "use_file_upload": true, + "max_file_size": 10, + "max_file_count": 5, + "allowed_extensions": [ + "jpg", + "jpeg", + "png", + "gif", + "pdf", + "zip" + ], + "add_to_menu": null, + "new_display_hours": 24, + "board_managers": [], + "board_steps": [], + "board_manager_ids": [], + "board_step_ids": [], + "notify_author": true, + "notify_admin_on_post": true, + "created_at": "2026-07-08 10:41:34", + "updated_at": "2026-07-08 10:41:34", + "permissions": null, + "category_post_counts": null, + "posts_count": 0, + "user_abilities": null, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + }, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -140,10 +238,79 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.board.store_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-board/admin/boards HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "slug": "example-key", + "description": [ + "예시 내용입니다." + ], + "is_active": true, + "type": "예시값", + "add_to_menu": true, + "per_page": 1, + "per_page_mobile": 1, + "order_by": "created_at", + "order_direction": "ASC", + "categories": [ + "예시값" + ], + "show_view_count": true, + "secret_mode": "disabled", + "use_comment": true, + "use_reply": true, + "use_report": true, + "comment_order": "ASC", + "new_display_hours": 1, + "min_title_length": 1, + "max_title_length": 1, + "min_content_length": 1, + "max_content_length": 1, + "min_comment_length": 1, + "max_comment_length": 1, + "use_file_upload": true, + "max_file_size": 1, + "max_file_count": 1, + "allowed_extensions": [ + "예시값" + ], + "board_manager_ids": [ + "예시값" + ], + "board_step_ids": [ + "예시값" + ], + "permissions": [ + "예시값" + ], + "max_reply_depth": 1, + "max_comment_depth": 1, + "notify_admin_on_post": true, + "notify_author": true, + "blocked_keywords": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -167,6 +334,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/boards/form-data HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -217,6 +393,281 @@ _단건 응답: `data` 객체의 필드._ | board_types | array | `[{"id":1,"slug":"basic","name":{"ko":"기본형","en":"Basic Li…` | 선택 가능한 게시판 타입 목록 (타입 선택 UI 렌더링용 — id/slug/name 등) | | _meta | object | `{"limits":{"per_page_min":5,"per_page_max":100,"min_title…` | 폼 입력 한계값 메타 (config('sirsoft-board.limits') — 페이지당 수·제목/본문/댓글 길이 등 min/max 범위) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시판 폼 데이터를 조회했습니다.", + "data": { + "type": "basic", + "per_page": 20, + "per_page_mobile": 15, + "order_by": "created_at", + "order_direction": "DESC", + "secret_mode": "{MASKED}", + "use_comment": true, + "use_reply": true, + "max_reply_depth": 5, + "max_comment_depth": 10, + "comment_order": "ASC", + "show_view_count": true, + "use_report": false, + "min_title_length": 2, + "max_title_length": 200, + "min_content_length": 2, + "max_content_length": 10000, + "min_comment_length": 2, + "max_comment_length": 1000, + "use_file_upload": false, + "max_file_size": 10, + "max_file_count": 5, + "notify_admin_on_post": true, + "notify_author": true, + "new_display_hours": 24, + "id": null, + "slug": null, + "is_active": true, + "add_to_menu": false, + "board_managers": [ + { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" + } + ], + "board_steps": [], + "board_manager_ids": [ + "a234c2b1-cde8-437f-b28b-23323be2b98d" + ], + "board_step_ids": [], + "created_at": null, + "updated_at": null, + "name": { + "ko": "" + }, + "description": { + "ko": "" + }, + "categories": [], + "blocked_keywords": [], + "allowed_extensions": [ + "jpg", + "jpeg", + "png", + "gif", + "webp", + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "ppt", + "pptx", + "hwp", + "txt", + "zip" + ], + "permissions": { + "admin_posts_read": { + "_key": "admin_posts_read", + "name": "admin.posts.read", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_posts_write": { + "_key": "admin_posts_write", + "name": "admin.posts.write", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_posts_read-secret": { + "_key": "admin_posts_read-secret", + "name": "admin.posts.read-secret", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_comments_read": { + "_key": "admin_comments_read", + "name": "admin.comments.read", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_comments_write": { + "_key": "admin_comments_write", + "name": "admin.comments.write", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_attachments_upload": { + "_key": "admin_attachments_upload", + "name": "admin.attachments.upload", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_attachments_download": { + "_key": "admin_attachments_download", + "name": "admin.attachments.download", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_manage": { + "_key": "admin_manage", + "name": "admin.manage", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "posts_read": { + "_key": "posts_read", + "name": "posts.read", + "mode": "roles", + "roles": [ + "admin", + "user", + "guest" + ] + }, + "posts_write": { + "_key": "posts_write", + "name": "posts.write", + "mode": "roles", + "roles": [ + "admin", + "user" + ] + }, + "posts_read-secret": { + "_key": "posts_read-secret", + "name": "posts.read-secret", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "comments_read": { + "_key": "comments_read", + "name": "comments.read", + "mode": "roles", + "roles": [ + "admin", + "user", + "guest" + ] + }, + "comments_write": { + "_key": "comments_write", + "name": "comments.write", + "mode": "roles", + "roles": [ + "admin", + "user" + ] + }, + "attachments_upload": { + "_key": "attachments_upload", + "name": "attachments.upload", + "mode": "roles", + "roles": [ + "admin", + "user" + ] + }, + "attachments_download": { + "_key": "attachments_download", + "name": "attachments.download", + "mode": "roles", + "roles": [ + "admin", + "user", + "guest" + ] + }, + "manager": { + "_key": "manager", + "name": "manager", + "mode": "roles", + "roles": [ + "admin" + ] + } + }, + "board_types": [ + { + "id": 2, + "slug": "basic", + "name": { + "ko": "기본형", + "en": "Basic List" + } + }, + { + "id": 3, + "slug": "gallery", + "name": { + "ko": "갤러리형", + "en": "Gallery" + } + }, + { + "id": 4, + "slug": "card", + "name": { + "ko": "카드형", + "en": "Card" + } + } + ], + "_meta": { + "limits": { + "per_page_min": 5, + "per_page_max": 100, + "min_title_length_min": 0, + "min_title_length_max": 200, + "max_title_length_min": 1, + "max_title_length_max": 1000, + "min_content_length_min": 0, + "min_content_length_max": 10000, + "max_content_length_min": 1, + "max_content_length_max": 100000, + "min_comment_length_min": 0, + "min_comment_length_max": 1000, + "max_comment_length_min": 1, + "max_comment_length_max": 10000, + "max_file_size_min": 1, + "max_file_size_max": 200, + "max_file_count_min": 1, + "max_file_count_max": 20, + "category_max": 50, + "max_reply_depth_min": 1, + "max_reply_depth_max": 10, + "max_comment_depth_min": 0, + "max_comment_depth_max": 10 + } + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -241,6 +692,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/boards/slug/apidoc-sample-board HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -293,6 +753,86 @@ _단건 응답: `data` 객체의 필드._ | user_abilities | object | `{"can_read":true,"can_write":true,"can_read_secret":true,…` | 현재 사용자의 게시판별 세부 권한 맵 (can_read/can_write/can_read_secret/can_read_comments/can_write_comments/can_upload/can_download/can_manage — 관리자 라우트에서는 항상 포함) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시판 정보를 조회했습니다.", + "data": { + "id": 1, + "name": "API 문서 샘플 게시판", + "slug": "apidoc-sample-board", + "is_active": true, + "type": "basic", + "description": "", + "per_page": 20, + "per_page_mobile": 15, + "order_by": "created_at", + "order_direction": "DESC", + "categories": [], + "show_view_count": true, + "secret_mode": "{MASKED}", + "use_comment": true, + "use_reply": true, + "max_reply_depth": 5, + "use_report": true, + "comment_order": "ASC", + "max_comment_depth": 10, + "min_title_length": 2, + "max_title_length": 200, + "min_content_length": 10, + "max_content_length": 10000, + "min_comment_length": 2, + "max_comment_length": 1000, + "blocked_keywords": [], + "use_file_upload": true, + "max_file_size": 10, + "max_file_count": 5, + "allowed_extensions": [ + "jpg", + "jpeg", + "png", + "gif", + "pdf", + "zip" + ], + "add_to_menu": null, + "new_display_hours": 24, + "board_managers": [], + "board_steps": [], + "board_manager_ids": [], + "board_step_ids": [], + "notify_author": true, + "notify_admin_on_post": true, + "created_at": "2026-07-08 10:41:34", + "updated_at": "2026-07-08 10:41:34", + "permissions": null, + "category_post_counts": null, + "posts_count": 0, + "user_abilities": { + "can_read": true, + "can_write": true, + "can_read_secret": "{MASKED}", + "can_read_comments": true, + "can_write_comments": true, + "can_upload": true, + "can_download": true, + "can_manage": true + }, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -318,9 +858,34 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | board | path | string | 예 | — | 대상 board의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-board/admin/boards/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시판이 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -347,6 +912,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | board | path | string | 예 | — | 대상 board의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/boards/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -399,6 +973,215 @@ _단건 응답: `data` 객체의 필드._ | user_abilities | null | `null` | 현재 사용자의 게시판별 세부 권한 맵 (can_read/can_write/can_read_secret/can_manage 등 — include_user_abilities 요청 시에만 채워지며 미포함 시 null) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시판 정보를 조회했습니다.", + "data": { + "id": 1, + "name": "API 문서 샘플 게시판", + "slug": "apidoc-sample-board", + "is_active": true, + "type": "basic", + "description": "", + "per_page": 20, + "per_page_mobile": 15, + "order_by": "created_at", + "order_direction": "DESC", + "categories": [], + "show_view_count": true, + "secret_mode": "{MASKED}", + "use_comment": true, + "use_reply": true, + "max_reply_depth": 5, + "use_report": true, + "comment_order": "ASC", + "max_comment_depth": 10, + "min_title_length": 2, + "max_title_length": 200, + "min_content_length": 10, + "max_content_length": 10000, + "min_comment_length": 2, + "max_comment_length": 1000, + "blocked_keywords": [], + "use_file_upload": true, + "max_file_size": 10, + "max_file_count": 5, + "allowed_extensions": [ + "jpg", + "jpeg", + "png", + "gif", + "pdf", + "zip" + ], + "add_to_menu": null, + "new_display_hours": 24, + "board_managers": [], + "board_steps": [], + "board_manager_ids": [], + "board_step_ids": [], + "notify_author": true, + "notify_admin_on_post": true, + "created_at": "2026-07-08 10:41:34", + "updated_at": "2026-07-08 10:41:34", + "permissions": { + "admin_posts_read": { + "_key": "admin_posts_read", + "name": "admin.posts.read", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_posts_write": { + "_key": "admin_posts_write", + "name": "admin.posts.write", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_posts_read-secret": { + "_key": "admin_posts_read-secret", + "name": "admin.posts.read-secret", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_comments_read": { + "_key": "admin_comments_read", + "name": "admin.comments.read", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_comments_write": { + "_key": "admin_comments_write", + "name": "admin.comments.write", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_attachments_upload": { + "_key": "admin_attachments_upload", + "name": "admin.attachments.upload", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_attachments_download": { + "_key": "admin_attachments_download", + "name": "admin.attachments.download", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "admin_manage": { + "_key": "admin_manage", + "name": "admin.manage", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "posts_read": { + "_key": "posts_read", + "name": "posts.read", + "mode": "roles", + "roles": [ + "admin", + "user", + "guest" + ] + }, + "posts_write": { + "_key": "posts_write", + "name": "posts.write", + "mode": "roles", + "roles": [ + "admin", + "user" + ] + }, + "posts_read-secret": { + "_key": "posts_read-secret", + "name": "posts.read-secret", + "mode": "roles", + "roles": [ + "admin" + ] + }, + "comments_read": { + "_key": "comments_read", + "name": "comments.read", + "mode": "roles", + "roles": [ + "admin", + "user", + "guest" + ] + }, + "comments_write": { + "_key": "comments_write", + "name": "comments.write", + "mode": "roles", + "roles": [ + "admin", + "user" + ] + }, + "attachments_upload": { + "_key": "attachments_upload", + "name": "attachments.upload", + "mode": "roles", + "roles": [ + "admin", + "user" + ] + }, + "attachments_download": { + "_key": "attachments_download", + "name": "attachments.download", + "mode": "roles", + "roles": [ + "admin", + "user", + "guest" + ] + }, + "manager": { + "_key": "manager", + "name": "manager", + "mode": "roles", + "roles": [ + "admin" + ] + } + }, + "category_post_counts": null, + "posts_count": 0, + "user_abilities": null, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -461,10 +1244,78 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.board.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-board/admin/boards/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "description": [ + "예시 내용입니다." + ], + "is_active": true, + "type": "예시값", + "add_to_menu": true, + "per_page": 1, + "per_page_mobile": 1, + "order_by": "created_at", + "order_direction": "ASC", + "categories": [ + "예시값" + ], + "show_view_count": true, + "secret_mode": "disabled", + "use_comment": true, + "use_reply": true, + "use_report": true, + "comment_order": "ASC", + "new_display_hours": 1, + "min_title_length": 1, + "max_title_length": 1, + "min_content_length": 1, + "max_content_length": 1, + "min_comment_length": 1, + "max_comment_length": 1, + "use_file_upload": true, + "max_file_size": 1, + "max_file_count": 1, + "allowed_extensions": [ + "예시값" + ], + "board_manager_ids": [ + "예시값" + ], + "board_step_ids": [ + "예시값" + ], + "permissions": [ + "예시값" + ], + "max_reply_depth": 1, + "max_comment_depth": 1, + "notify_admin_on_post": true, + "notify_author": true, + "blocked_keywords": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -491,9 +1342,54 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | board | path | string | 예 | — | 대상 board의 식별자 | +**요청 예시** + +```http +POST /api/modules/sirsoft-board/admin/boards/1/add-to-menu HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| menu | object | `{"name":{"ko":"API 문서 샘플 게시판 게시판","en":"API Doc Sample Bo…` | 이 게시판용으로 새로 생성된 관리자 사이드바 메뉴 항목 (name/slug/url/icon/parent_id/order 등 Menu 레코드 전체) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "관리자 메뉴에 추가되었습니다.", + "data": { + "menu": { + "name": { + "ko": "API 문서 샘플 게시판 게시판", + "en": "API Doc Sample Board Board" + }, + "slug": "board-apidoc-sample-board", + "url": "/admin/board/apidoc-sample-board", + "icon": "fas fa-clipboard-list", + "parent_id": 15, + "is_active": true, + "extension_type": "module", + "extension_identifier": "sirsoft-board", + "order": 4, + "updated_at": "2026-07-08T06:01:45.000000Z", + "created_at": "2026-07-08T06:01:45.000000Z", + "id": 27 + } + } +} +``` **에러 응답** @@ -518,6 +1414,15 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-board/boards HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -529,6 +1434,33 @@ _요청 파라미터 없음._ | posts_count | integer | `0` | posts 개수 (집계) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "성공적으로 처리되었습니다.", + "data": [ + { + "id": 1, + "name": "API 문서 샘플 게시판", + "slug": "apidoc-sample-board", + "description": "", + "posts_count": 0, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } + ] +} +``` + **에러 응답** _대표 에러 없음 (공개 조회). _ @@ -548,6 +1480,15 @@ _대표 에러 없음 (공개 조회). _ @@ -575,12 +1536,56 @@ _대표 에러 없음 (공개 조회). +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "성공적으로 처리되었습니다.", + "data": [ + { + "id": 1, + "board_slug": "apidoc-sample-board", + "board_name": "API 문서 샘플 게시판", + "title": "API 문서 샘플 게시글", + "excerpt": "API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.", + "author": { + "id": 6, + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com", + "is_guest": false, + "avatar": null, + "status": "active", + "status_label": "활성" + }, + "view_count": 44, + "comment_count": 0, + "created_at": "2026-07-08 10:41:34", + "created_at_formatted": "4시간 전" + } + ] +} +``` + **에러 응답** _대표 에러 없음 (공개 조회). _ @@ -600,6 +1605,15 @@ _대표 에러 없음 (공개 조회). _ @@ -629,6 +1665,15 @@ _대표 에러 없음 (공개 조회). _ @@ -664,6 +1737,15 @@ _대표 에러 없음 (공개 조회). _ @@ -696,6 +1797,15 @@ _대표 에러 없음 (공개 조회). +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -801,10 +2133,23 @@ _단건 응답: `data` 객체의 필드._ | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/boards/apidoc-sample-board/attachment/apidocsmpl1/preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -834,10 +2179,43 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.attachment.upload_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-board/boards/apidoc-sample-board/attachments HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary +Content-Disposition: form-data; name="post_id" + +1 +------G7ExampleBoundary +Content-Disposition: form-data; name="collection" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="temp_key" + +예시값 +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -866,10 +2244,30 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.attachment.reorder_validation_rules`). +**요청 예시** + +```http +PATCH /api/modules/sirsoft-board/boards/apidoc-sample-board/attachments/reorder HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +Content-Type: application/json + +{ + "order": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -896,9 +2294,34 @@ _단건 응답: `data` 객체의 필드._ | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-board/boards/apidoc-sample-board/attachments/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "파일이 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -927,10 +2350,29 @@ _단건 응답: `data` 객체의 필드._ | reason_type | body | string | 예 | — | 신고 사유 유형 (ReportReasonType Enum 값 중 하나 — 스팸/욕설 등) | | reason_detail | body | string | 예 | min 1, max 1000 | 신고 사유 상세 설명 (1~1000자) | +**요청 예시** + +```http +POST /api/modules/sirsoft-board/boards/apidoc-sample-board/comments/1/reports HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "reason_type": "예시값", + "reason_detail": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -957,10 +2399,23 @@ _단건 응답: `data` 객체의 필드._ | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | commentId | path | string | 예 | — | 대상 comment의 식별자 | +**요청 예시** + +```http +POST /api/modules/sirsoft-board/boards/apidoc-sample-board/comments/1/verify-password HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -985,6 +2440,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/boards/apidoc-sample-board/posts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -1021,6 +2485,106 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | number | integer | `1` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | | show_category | boolean | `false` | 게시판에 분류가 설정돼 있어 목록에 분류 컬럼을 노출할지 여부 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시글 목록을 조회했습니다.", + "data": { + "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": "{MASKED}", + "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": "/api/modules/sirsoft-board/boards/apidoc-sample-board/attachment/apidocsmpl1/preview", + "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, + "slug": "apidoc-sample-board", + "title": "API 문서 샘플 게시글", + "deleted_at": null, + "content_preview": "API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.", + "row_type": "normal", + "number": 1, + "show_category": false + } + ], + "pagination": { + "total": 1, + "all_total": 1, + "count": 1, + "per_page": 25, + "current_page": 1, + "last_page": 1, + "from": 1, + "to": 1, + "has_more_pages": false + }, + "board": { + "slug": "apidoc-sample-board", + "name": "API 문서 샘플 게시판", + "description": "", + "type": "basic", + "categories": [], + "show_category": false, + "settings": { + "use_file_upload": true, + "use_comment": true, + "use_reply": true, + "use_report": true, + "secret_mode": "{MASKED}", + "show_view_count": true, + "per_page": 20, + "posts_per_page": 20, + "posts_per_page_mobile": 15, + "new_display_hours": 24, + "order_by": "created_at", + "order_direction": "DESC" + } + }, + "abilities": { + "can_read": true, + "can_write": true, + "can_read_secret": "{MASKED}", + "can_read_comments": true, + "can_write_comments": true, + "can_upload": true, + "can_download": true, + "can_manage": true, + "can_view_deleted": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1047,10 +2611,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.user_post.store_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-board/boards/apidoc-sample-board/posts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1076,6 +2653,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/boards/apidoc-sample-board/posts/form-data HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -1090,6 +2676,28 @@ _단건 응답: `data` 객체의 필드._ | is_secret | boolean | `false` | secret 여부 | | parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시글 폼 데이터를 조회했습니다.", + "data": { + "title": "", + "content": "", + "content_mode": "text", + "category": null, + "is_notice": false, + "is_secret": "{MASKED}", + "parent_id": null + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1114,6 +2722,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/boards/apidoc-sample-board/posts/form-meta HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -1122,6 +2739,88 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | board | object | `{"id":12,"name":"API 문서 샘플 게시판","slug":"apidoc-sample-boa…` | 글쓰기/수정 폼 렌더링용 게시판 메타 (id/name/slug/type + 게시판 설정과 현재 사용자 권한(user_abilities) 등) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시글 폼 메타 데이터를 조회했습니다.", + "data": { + "board": { + "id": 1, + "name": "API 문서 샘플 게시판", + "slug": "apidoc-sample-board", + "is_active": true, + "type": "basic", + "description": "", + "per_page": 20, + "per_page_mobile": 15, + "order_by": "created_at", + "order_direction": "DESC", + "categories": [], + "show_view_count": true, + "secret_mode": "{MASKED}", + "use_comment": true, + "use_reply": true, + "max_reply_depth": 5, + "use_report": true, + "comment_order": "ASC", + "max_comment_depth": 10, + "min_title_length": 2, + "max_title_length": 200, + "min_content_length": 10, + "max_content_length": 10000, + "min_comment_length": 2, + "max_comment_length": 1000, + "blocked_keywords": [], + "use_file_upload": true, + "max_file_size": 10, + "max_file_count": 5, + "allowed_extensions": [ + "jpg", + "jpeg", + "png", + "gif", + "pdf", + "zip" + ], + "add_to_menu": null, + "new_display_hours": 24, + "board_managers": [], + "board_steps": [], + "board_manager_ids": [], + "board_step_ids": [], + "notify_author": true, + "notify_admin_on_post": true, + "created_at": "2026-07-08 10:41:34", + "updated_at": "2026-07-08 10:41:34", + "permissions": null, + "category_post_counts": null, + "posts_count": 0, + "user_abilities": { + "can_read": true, + "can_write": true, + "can_read_secret": "{MASKED}", + "can_read_comments": true, + "can_write_comments": true, + "can_upload": true, + "can_download": true, + "can_manage": true + }, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1147,9 +2846,34 @@ _단건 응답: `data` 객체의 필드._ | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-board/boards/apidoc-sample-board/posts/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시글이 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -1176,6 +2900,15 @@ _단건 응답: `data` 객체의 필드._ | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/boards/apidoc-sample-board/posts/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -1222,6 +2955,175 @@ _단건 응답: `data` 객체의 필드._ | is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_read":true,"can_write":true,"can_read_secret":true,…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```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": "{MASKED}", + "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": "/api/modules/sirsoft-board/boards/apidoc-sample-board/attachment/apidocsmpl1/preview", + "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": "

API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.

", + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "trigger_type": "user", + "updated_at": "2026-07-08 15:01:44", + "deleted_at": null, + "ip_address": "127.0.0.1", + "action_logs": [], + "board": { + "slug": "apidoc-sample-board", + "name": "API 문서 샘플 게시판", + "type": "basic", + "use_comment": true, + "use_reply": true, + "use_report": true, + "show_view_count": true, + "max_reply_depth": 5, + "max_comment_depth": 10, + "report_types": [ + { + "value": "abuse", + "label": "욕설/비방" + }, + { + "value": "hate_speech", + "label": "혐오 발언" + }, + { + "value": "spam", + "label": "스팸/광고" + }, + { + "value": "copyright", + "label": "저작권 침해" + }, + { + "value": "privacy", + "label": "개인정보 노출" + }, + { + "value": "misinformation", + "label": "허위정보" + }, + { + "value": "sexual", + "label": "성적인 콘텐츠" + }, + { + "value": "violence", + "label": "폭력적인 콘텐츠" + }, + { + "value": "other", + "label": "기타" + } + ] + }, + "navigation": null, + "parent": null, + "comments": [ + { + "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": "{MASKED}", + "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 10:41:34", + "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_write": true + } + } + ], + "attachments": [ + { + "id": 1, + "hash": "apidocsmpl1", + "original_filename": "apidoc-sample.png", + "mime_type": "image/png", + "size": 2048, + "size_formatted": "2 KB", + "collection": "default", + "order": 0, + "download_url": "/api/modules/sirsoft-board/boards/apidoc-sample-board/attachment/apidocsmpl1", + "preview_url": "/api/modules/sirsoft-board/boards/apidoc-sample-board/attachment/apidocsmpl1/preview", + "is_image": true, + "meta": null + } + ], + "replies": [], + "is_already_reported": false, + "is_owner": true, + "abilities": { + "can_read": true, + "can_write": true, + "can_read_secret": "{MASKED}", + "can_read_comments": true, + "can_write_comments": true, + "can_upload": true, + "can_download": true, + "can_manage": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1249,9 +3151,179 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.user_post.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-board/boards/apidoc-sample-board/posts/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| category | null | `null` | 게시글이 속한 분류(카테고리) 이름 (분류 미지정 시 null) | +| author | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 작성자 사용자 객체 (uuid/name — author 관계 파생) | +| is_notice | boolean | `false` | notice 여부 | +| is_secret | boolean | `false` | secret 여부 | +| content_mode | string | `html` | 본문 편집 모드 (html: HTML 에디터 / text: 일반 텍스트 — 미지정 시 text) | +| is_new | boolean | `true` | new 여부 | +| status | string | `published` | 상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조) | +| status_label | string | `게시됨` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| view_count | integer | `44` | view 개수 (집계) | +| comment_count | integer | `0` | comment 개수 (집계) | +| reply_count | integer | `0` | reply 개수 (집계) | +| attachment_count | integer | `0` | attachment 개수 (집계) | +| has_attachment | boolean | `false` | attachment 여부 | +| thumbnail | null | `null` | 썸네일 이미지 URL/경로 | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| is_reply | boolean | `false` | reply 여부 | +| created_at | string | `2026-07-08 10:41:34` | 생성 일시 | +| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| is_author | boolean | `true` | author 여부 | +| is_guest_post | boolean | `false` | guest post 여부 | +| title | string | `API 문서 샘플 게시글` | 제목 | +| content | string | `

API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.

` | 본문 내용 | +| user_id | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | user 식별자 (연관 리소스 참조) | +| trigger_type | string | `user` | 동작을 유발한 방식/주체 구분 값 | +| updated_at | string | `2026-07-08 15:01:44` | 최종 수정 일시 | +| deleted_at | null | `null` | 소프트 삭제 일시 (미삭제 시 null) | +| ip_address | string | `127.0.0.1` | 요청/행위가 발생한 IP 주소 | +| action_logs | array | `[]` | 게시글 블라인드/복원/삭제 등 운영 처리 이력 (action/reason/admin_name/created_at — admin.manage 권한 보유자에게만 노출, 비권한자는 null) | +| board | object | `{"slug":"apidoc-sample-board","name":"API 문서 샘플 게시판","typ…` | 소속 게시판 요약 정보 (slug/name/type/댓글·답변·신고 사용 여부·최대 깊이·신고 유형 목록 등 — board 관계 로드 시에만 채워지며 미로드 시 null) | +| navigation | null | `null` | 이전/다음 글 이동 정보 (별도 navigation 조회로 채워지며, 이 응답에는 기본적으로 미포함되어 null) | +| parent | null | `null` | 상위 항목 객체 (parent 관계 파생) | +| comments | null | `null` | 게시글 댓글 목록 (comments 관계 로드 시 CommentResource 컬렉션으로 채워지며, 미로드 시 null) | +| attachments | null | `null` | 게시글 첨부파일 목록 (attachments 관계 로드 시 권한·비밀글·삭제 정책에 따라 필터링된 AttachmentResource 컬렉션, 미로드 시 null) | +| replies | null | `null` | 이 게시글에 달린 답변글 목록 (replies 관계 로드 시 동일 PostResource 컬렉션으로 채워지며, 미로드 시 null) | +| is_already_reported | boolean | `false` | already reported 여부 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_write":true,"can_read_secret":true,…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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": "{MASKED}", + "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": "

API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.

", + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "trigger_type": "user", + "updated_at": "2026-07-08 15:01:44", + "deleted_at": null, + "ip_address": "127.0.0.1", + "action_logs": [], + "board": { + "slug": "apidoc-sample-board", + "name": "API 문서 샘플 게시판", + "type": "basic", + "use_comment": true, + "use_reply": true, + "use_report": true, + "show_view_count": true, + "max_reply_depth": 5, + "max_comment_depth": 10, + "report_types": [ + { + "value": "abuse", + "label": "욕설/비방" + }, + { + "value": "hate_speech", + "label": "혐오 발언" + }, + { + "value": "spam", + "label": "스팸/광고" + }, + { + "value": "copyright", + "label": "저작권 침해" + }, + { + "value": "privacy", + "label": "개인정보 노출" + }, + { + "value": "misinformation", + "label": "허위정보" + }, + { + "value": "sexual", + "label": "성적인 콘텐츠" + }, + { + "value": "violence", + "label": "폭력적인 콘텐츠" + }, + { + "value": "other", + "label": "기타" + } + ] + }, + "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": "{MASKED}", + "can_read_comments": true, + "can_write_comments": true, + "can_upload": true, + "can_download": true, + "can_manage": true + } + } +} +``` **에러 응답** @@ -1279,6 +3351,15 @@ _단건 응답: `data` 객체의 필드._ | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/boards/apidoc-sample-board/posts/1/navigation HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -1288,6 +3369,23 @@ _단건 응답: `data` 객체의 필드._ | prev | null | `null` | 정렬 기준상 이전 글 정보 (없거나 공지·답글·조회 실패 시 null) | | next | null | `null` | 정렬 기준상 다음 글 정보 (없거나 공지·답글·조회 실패 시 null) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시글 목록을 조회했습니다.", + "data": { + "prev": null, + "next": null + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1314,10 +3412,28 @@ _단건 응답: `data` 객체의 필드._ | id | path | string | 예 | — | 대상 리소스의 식별자 | | password | body | string | 예 | — | 비밀번호 | +**요청 예시** + +```http +POST /api/modules/sirsoft-board/boards/apidoc-sample-board/posts/1/verify-password HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +Content-Type: application/json + +{ + "password": "Password123!" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1345,10 +3461,28 @@ _단건 응답: `data` 객체의 필드._ | id | path | string | 예 | — | 대상 리소스의 식별자 | | password | body | string | 예 | — | 비밀번호 | +**요청 예시** + +```http +POST /api/modules/sirsoft-board/boards/apidoc-sample-board/posts/1/verify-password-for-modify HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +Content-Type: application/json + +{ + "password": "Password123!" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1375,6 +3509,15 @@ _단건 응답: `data` 객체의 필드._ | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | postId | path | string | 예 | — | 대상 post의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/boards/apidoc-sample-board/posts/1/comments HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -1402,6 +3545,55 @@ _단건 응답: `data` 객체의 필드._ | is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_write":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-board::messages.comments.index_success", + "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": "{MASKED}", + "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 10:41:34", + "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_write": true + } + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1429,10 +3621,23 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.comment.store_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-board/boards/apidoc-sample-board/posts/1/comments HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1460,9 +3665,34 @@ _단건 응답: `data` 객체의 필드._ | postId | path | string | 예 | — | 대상 post의 식별자 | | commentId | path | string | 예 | — | 대상 comment의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-board/boards/apidoc-sample-board/posts/1/comments/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "댓글이 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -1492,10 +3722,23 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.comment.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-board/boards/apidoc-sample-board/posts/1/comments/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1524,10 +3767,29 @@ _단건 응답: `data` 객체의 필드._ | reason_type | body | string | 예 | — | 신고 사유 유형 (ReportReasonType Enum 값 중 하나 — 스팸/욕설 등) | | reason_detail | body | string | 예 | min 1, max 1000 | 신고 사유 상세 설명 (1~1000자) | +**요청 예시** + +```http +POST /api/modules/sirsoft-board/boards/apidoc-sample-board/posts/1/reports HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "reason_type": "예시값", + "reason_detail": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-board/docs/api/dashboard.md b/modules/_bundled/sirsoft-board/docs/api/dashboard.md index ce2469d1..2f3813e8 100644 --- a/modules/_bundled/sirsoft-board/docs/api/dashboard.md +++ b/modules/_bundled/sirsoft-board/docs/api/dashboard.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/dashboard/overview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -36,6 +45,23 @@ _단건 응답: `data` 객체의 필드._ | today_posts | integer | `0` | 오늘 등록된 새 게시글 수입니다. board_stats 집계 테이블의 오늘 행에서 읽으며, 행이 없으면 0 을 반환합니다(최대 1시간 지연). | | today_comments | integer | `0` | 오늘 등록된 새 댓글 수입니다. board_stats 집계 테이블의 오늘 행에서 읽으며, 행이 없으면 0 을 반환합니다(최대 1시간 지연). | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-board::messages.dashboard.fetch_success", + "data": { + "today_posts": 0, + "today_comments": 0 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -59,6 +85,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | limit | query | integer | 아니오 | min 1, max 50 | 반환할 최대 항목 수 | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/dashboard/pending-reports?limit=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -68,6 +103,38 @@ _단건 응답: `data` 객체의 필드._ | items | array | `[{"id":7,"board_slug":"qna","board_name":"Q&A","target_ty…` | 전체 게시판을 가로질러 조회한 미처리 신고 항목 목록입니다. 각 항목은 신고 대상 게시판(board_slug/board_name), 대상 종류(target_type), 대상 제목/발췌(target_title/target_excerpt), 상태, 신고 시각을 포함합니다. `limit` 개까지 반환됩니다. | | total | integer | `16` | 전체 개수 (집계) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-board::messages.dashboard.fetch_success", + "data": { + "items": [ + { + "id": 1, + "board_slug": "apidoc-sample-board", + "board_name": "API 문서 샘플 게시판", + "target_type": "post", + "target_type_label": "게시글", + "target_post_id": 1, + "target_title": "API 문서 샘플 게시글", + "target_excerpt": null, + "status": "pending", + "status_label": "접수", + "author_name": "API 문서 샘플 사용자", + "last_reported_at": "4시간 전" + } + ], + "total": 1 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -90,6 +157,15 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/dashboard/post-graph HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -104,6 +180,64 @@ _단건 응답: `data` 객체의 필드._ | updated_at | null | `null` | 최종 수정 일시 | | updated_at_display | string | `` | 집계 행의 최종 갱신 시각(updated_at)을 모듈 날짜 표시 형식(display.date_display_format 설정)으로 포맷한 표시용 문자열입니다. 집계 행이 없으면 빈 문자열입니다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-board::messages.dashboard.fetch_success", + "data": { + "days": [ + { + "date": "2026-07-02", + "post_count": 0, + "comment_count": 0 + }, + { + "date": "2026-07-03", + "post_count": 0, + "comment_count": 0 + }, + { + "date": "2026-07-04", + "post_count": 0, + "comment_count": 0 + }, + { + "date": "2026-07-05", + "post_count": 0, + "comment_count": 0 + }, + { + "date": "2026-07-06", + "post_count": 0, + "comment_count": 0 + }, + { + "date": "2026-07-07", + "post_count": 0, + "comment_count": 0 + }, + { + "date": "2026-07-08", + "post_count": 0, + "comment_count": 0 + } + ], + "total_posts": 0, + "total_comments": 0, + "posts_change": null, + "comments_change": null, + "updated_at": null, + "updated_at_display": "" + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -127,6 +261,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | limit | query | integer | 아니오 | min 1, max 50 | 반환할 최대 항목 수 | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/dashboard/recent-posts?limit=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -139,6 +282,30 @@ _단건 응답: `data` 객체의 필드._ | comments_count | integer | `0` | comments 개수 (집계) | | created_at | string | `4시간 전` | 생성 일시 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-board::messages.dashboard.fetch_success", + "data": [ + { + "id": 1, + "board_slug": "apidoc-sample-board", + "board_name": "API 문서 샘플 게시판", + "title": "API 문서 샘플 게시글", + "author_name": "API 문서 샘플 사용자", + "comments_count": 0, + "created_at": "4시간 전" + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-board/docs/api/my-comments.md b/modules/_bundled/sirsoft-board/docs/api/my-comments.md index 8c282591..3da32a33 100644 --- a/modules/_bundled/sirsoft-board/docs/api/my-comments.md +++ b/modules/_bundled/sirsoft-board/docs/api/my-comments.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-board/me/my-comments HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -42,6 +51,69 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | created_at | string | `2026-07-07 09:34:50` | 생성 일시 | | created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "내 활동 게시글을 조회했습니다.", + "data": { + "current_page": 1, + "data": [ + { + "id": 1, + "board_slug": "apidoc-sample-board", + "board_name": "API 문서 샘플 게시판", + "post_title": "API 문서 샘플 게시글", + "post_id_val": 1, + "content": "API 문서 샘플 댓글입니다.", + "created_at": "2026-07-08 10:41:34", + "created_at_formatted": "4시간 전" + } + ], + "first_page_url": "https://api.example.com/api/modules/sirsoft-board/me/my-comments?page=1", + "from": 1, + "last_page": 1, + "last_page_url": "https://api.example.com/api/modules/sirsoft-board/me/my-comments?page=1", + "links": [ + { + "url": null, + "label": "pagination.previous", + "page": null, + "active": false + }, + { + "url": "https://api.example.com/api/modules/sirsoft-board/me/my-comments?page=1", + "label": "1", + "page": 1, + "active": true + }, + { + "url": null, + "label": "pagination.next", + "page": null, + "active": false + } + ], + "next_page_url": null, + "path": "https://api.example.com/api/modules/sirsoft-board/me/my-comments", + "per_page": 25, + "prev_page_url": null, + "to": 1, + "total": 1, + "query": { + "board_slug": "", + "search": "", + "sort": "latest" + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-board/docs/api/reports.md b/modules/_bundled/sirsoft-board/docs/api/reports.md index 64d86e6a..ab51e2b3 100644 --- a/modules/_bundled/sirsoft-board/docs/api/reports.md +++ b/modules/_bundled/sirsoft-board/docs/api/reports.md @@ -38,6 +38,15 @@ | sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | | per_page | query | integer | 아니오 | — | 페이지당 항목 수 | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/reports?filters=%EC%98%88%EC%8B%9C%EA%B0%92&status=%EC%98%88%EC%8B%9C%EA%B0%92&target_type=%EC%98%88%EC%8B%9C%EA%B0%92&target_status=%EC%98%88%EC%8B%9C%EA%B0%92&board_id=1&reported_at_from=%EC%98%88%EC%8B%9C%EA%B0%92&reported_at_to=%EC%98%88%EC%8B%9C%EA%B0%92&sort_by=%EC%98%88%EC%8B%9C%EA%B0%92&sort_order=asc&per_page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -75,6 +84,92 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_view":true,"can_manage":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "신고 목록을 조회했습니다.", + "data": { + "data": [ + { + "id": 1, + "board_id": 1, + "board": { + "id": 1, + "name": "API 문서 샘플 게시판", + "slug": "apidoc-sample-board", + "title": null, + "current_status": null, + "deleted_at": null + }, + "target_type": "post", + "target_type_label": "게시글", + "target_id": 1, + "post_id": 1, + "content": null, + "content_mode": "text", + "content_preview": "", + "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": "pending", + "status_label": "접수", + "status_variant": "warning", + "processor": null, + "processed_at": null, + "metadata": null, + "report_count": 0, + "last_reported_at": "2026-07-08 10:41:34", + "is_reactivated": false, + "target_status": "published", + "target_trigger_type": "user", + "target_status_label": "게시중", + "created_at": "2026-07-08 10:41:34", + "updated_at": "2026-07-08 10:41:34", + "is_owner": false, + "abilities": { + "can_view": true, + "can_manage": true + } + } + ], + "statistics": { + "by_status": { + "pending": 1 + }, + "by_type": { + "post": 1 + }, + "total": 1 + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 20, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + }, + "abilities": { + "can_view": true, + "can_manage": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -102,10 +197,32 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | status | body | string | 예 | — | 일괄 전환할 신고 상태 (`ReportStatus` 허용값: pending/review/rejected/suspended 등). 지정한 모든 신고를 이 상태로 변경합니다. | | process_note | body | string | 아니오 | max 1000 | 처리 메모 (최대 1000자). 상태 변경 사유나 조치 내용을 처리 이력에 함께 기록합니다. | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-board/admin/reports/bulk-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "status": "예시값", + "process_note": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -132,10 +249,31 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | | target_status | body | string | 아니오 | — | 집계 기준이 되는 전환 대상 상태 (`ReportStatus` 허용값). 지정 시 해당 상태로의 일괄 전환을 가정한 상태별 건수 요약을 계산합니다. | +**요청 예시** + +```http +POST /api/modules/sirsoft-board/admin/reports/status-counts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "target_status": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -161,9 +299,34 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | report | path | string | 예 | — | 대상 report의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-board/admin/reports/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "신고가 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -190,6 +353,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | report | path | string | 예 | — | 대상 report의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/reports/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -220,6 +392,70 @@ _단건 응답: `data` 객체의 필드._ | created_at | string | `2026-06-04 09:35:42` | 생성 일시 | | updated_at | string | `2026-06-04 09:35:42` | 최종 수정 일시 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "신고 목록을 조회했습니다.", + "data": { + "id": 1, + "board_id": 1, + "board": { + "id": 1, + "name": "API 문서 샘플 게시판", + "slug": "apidoc-sample-board" + }, + "target_type": "post", + "target_id": 1, + "post": { + "id": 1, + "title": "API 문서 샘플 게시글", + "content": "

API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.

", + "content_mode": "html", + "created_at": "2026-07-08 10:41:34", + "author": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com", + "is_guest": false + } + }, + "comment": null, + "target_status": "published", + "target_status_label": "게시중", + "blind_trigger_type": "user", + "blind_trigger_type_label": "사용자 직접 삭제", + "status": "pending", + "available_actions": [ + "review", + "rejected", + "suspended", + "deleted" + ], + "abilities": { + "can_view": true, + "can_manage": true + }, + "reporters": [], + "report_count": 0, + "reason_summary": "-", + "first_reported_at": "2026-07-08 10:41:34", + "last_reported_at": "2026-07-08 10:41:34", + "histories": [], + "metadata": { + "reason": "spam" + }, + "created_at": "2026-07-08 10:41:34", + "updated_at": "2026-07-08 10:41:34" + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -247,6 +483,15 @@ _단건 응답: `data` 객체의 필드._ | per_page | query | integer | 아니오 | min 1 | 페이지당 항목 수 | | page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/reports/1/reporters?per_page=1&page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -261,6 +506,30 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | snapshot | object | `{"board_name":"갤러리","title":"작업물 공유합니다","content":"최근에 작업…` | 신고 접수 시점의 대상 콘텐츠 스냅샷 (게시판명/제목/본문/작성자 등). 이후 대상이 수정·삭제되어도 신고 당시 내용을 보존합니다. | | reported_at | string | `2026-06-04 09:35:42` | reported 일시 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "신고 목록을 조회했습니다.", + "data": { + "data": [], + "pagination": { + "total": 0, + "from": 0, + "to": 0, + "per_page": 25, + "current_page": 1, + "last_page": 1 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -289,10 +558,29 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | status | body | string | 예 | — | 전환할 신고 상태 (`ReportStatus` 허용값). 현재 상태에서 전환 불가한 값(영구삭제 등)은 검증에서 422로 차단됩니다. | | process_note | body | string | 아니오 | max 1000 | 처리 메모 (최대 1000자). 상태 변경 사유나 조치 내용을 처리 이력에 함께 기록합니다. | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-board/admin/reports/1/status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "status": "예시값", + "process_note": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-board/docs/api/settings.md b/modules/_bundled/sirsoft-board/docs/api/settings.md index 83e98870..87c93cc2 100644 --- a/modules/_bundled/sirsoft-board/docs/api/settings.md +++ b/modules/_bundled/sirsoft-board/docs/api/settings.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/settings HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -43,6 +52,291 @@ _단건 응답: `data` 객체의 필드._ | abilities | object | `{"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | | _meta | object | `{"limits":{"per_page_min":5,"per_page_max":100,"min_title…` | 편집 UI 보조 메타데이터. `limits`에 `config('sirsoft-board.limits')` 기반 입력 제한값(페이지당 글 수·답글/댓글 깊이의 최소·최대 등)이 담겨 프론트 입력 검증 범위로 사용됩니다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-board::messages.settings.fetch_success", + "data": { + "basic_defaults": { + "type": "basic", + "per_page": 20, + "per_page_mobile": 15, + "order_by": "created_at", + "order_direction": "DESC", + "secret_mode": "{MASKED}", + "use_comment": true, + "use_reply": true, + "max_reply_depth": 5, + "max_comment_depth": 10, + "comment_order": "ASC", + "show_view_count": true, + "use_report": false, + "min_title_length": 2, + "max_title_length": 200, + "min_content_length": 2, + "max_content_length": 10000, + "min_comment_length": 2, + "max_comment_length": 1000, + "use_file_upload": false, + "max_file_size": 10, + "max_file_count": 5, + "allowed_extensions": [ + "jpg", + "jpeg", + "png", + "gif", + "webp", + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "ppt", + "pptx", + "hwp", + "txt", + "zip" + ], + "notify_admin_on_post": true, + "notify_author": true, + "new_display_hours": 24, + "blocked_keywords": [ + "시발", + "씨발", + "ㅅㅂ", + "ㅆㅂ", + "좆", + "ㅈㄴ", + "존나", + "씹", + "지랄", + "ㅈㄹ", + "개새끼", + "새끼", + "ㅅㄲ", + "개자식", + "병신", + "ㅂㅅ", + "미친놈", + "미친년", + "느금마", + "니애미", + "느금", + "엠창", + "엠뒤", + "빡대가리", + "개같은", + "개년", + "개놈", + "개돼지", + "개씹", + "개쓰레기", + "닥쳐", + "꺼져", + "죽어", + "자살해", + "뒤져", + "저능아", + "정신병자", + "썅", + "쌍놈", + "쌍년", + "쉬발", + "쒸발", + "시팔", + "씨팔", + "한남", + "한녀", + "김치녀", + "된장녀", + "맘충", + "틀딱", + "느개비", + "장애인놈", + "병신새끼", + "흑형", + "짱깨", + "쪽바리", + "왜놈", + "튀기", + "똥남아", + "야동", + "포르노", + "섹스", + "야사", + "음란", + "성인사이트", + "조건만남", + "원나잇", + "섹파", + "오피", + "키스방", + "매춘", + "성매매", + "몸파는" + ], + "default_board_permissions": { + "admin.posts.read": [ + "admin" + ], + "admin.posts.write": [ + "admin" + ], + "admin.posts.read-secret": [ + "admin" + ], + "admin.comments.read": [ + "admin" + ], + "admin.comments.write": [ + "admin" + ], + "admin.attachments.upload": [ + "admin" + ], + "admin.attachments.download": [ + "admin" + ], + "admin.manage": [ + "admin" + ], + "posts.read": [ + "admin", + "user", + "guest" + ], + "posts.write": [ + "admin", + "user" + ], + "posts.read-secret": [ + "admin" + ], + "comments.read": [ + "admin", + "user", + "guest" + ], + "comments.write": [ + "admin", + "user" + ], + "attachments.upload": [ + "admin", + "user" + ], + "attachments.download": [ + "admin", + "user", + "guest" + ], + "manager": [ + "admin" + ] + } + }, + "report_policy": { + "auto_hide_threshold": 5, + "auto_hide_target": "both", + "daily_report_limit": 10, + "rejection_limit_count": 5, + "rejection_limit_days": 30, + "notify_admin_on_report": true, + "notify_admin_on_report_scope": "per_case", + "notify_admin_on_report_channels": [ + "mail", + "database" + ], + "notify_author_on_report_action": true, + "notify_author_on_report_action_channels": [ + "mail", + "database" + ] + }, + "spam_security": { + "post_cooldown_seconds": 0, + "comment_cooldown_seconds": 0, + "report_cooldown_seconds": 60, + "view_count_cache_ttl": 86400 + }, + "display": { + "date_display_format": "standard" + }, + "seo": { + "meta_boards_title": "{site_name}", + "meta_boards_description": "", + "meta_board_title": "{board_name}", + "meta_board_description": "{board_description}", + "meta_post_title": "{board_name} - {post_title}", + "meta_post_description": "", + "seo_boards": true, + "seo_board": true, + "seo_post_detail": true + }, + "notifications": { + "channels": [ + { + "id": "mail", + "is_active": true, + "sort_order": 0 + }, + { + "id": "database", + "is_active": true, + "sort_order": 1 + } + ] + }, + "report_permissions": { + "view_roles": [ + "admin", + "apidoc-sample-role" + ], + "manage_roles": [ + "admin", + "apidoc-sample-role" + ] + }, + "abilities": { + "can_update": true + }, + "_meta": { + "limits": { + "per_page_min": 5, + "per_page_max": 100, + "min_title_length_min": 0, + "min_title_length_max": 200, + "max_title_length_min": 1, + "max_title_length_max": 1000, + "min_content_length_min": 0, + "min_content_length_max": 10000, + "max_content_length_min": 1, + "max_content_length_max": 100000, + "min_comment_length_min": 0, + "min_comment_length_max": 1000, + "max_comment_length_min": 1, + "max_comment_length_max": 10000, + "max_file_size_min": 1, + "max_file_size_max": 200, + "max_file_count_min": 1, + "max_file_count_max": 20, + "category_max": 50, + "max_reply_depth_min": 1, + "max_reply_depth_max": 10, + "max_comment_depth_min": 0, + "max_comment_depth_max": 10 + } + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -74,9 +368,303 @@ _단건 응답: `data` 객체의 필드._ | spam_security | body | array | 아니오 | — | 스팸·보안 카테고리 값. 글·댓글·신고 작성 쿨다운 시간(초)과 조회수 캐시 TTL을 저장합니다. | | seo | body | array | 아니오 | — | SEO 설정 카테고리 값. 게시판 목록/개별/글 상세 페이지의 메타 제목·설명 템플릿과 각 페이지 SEO 활성화 여부를 저장합니다. | +**요청 예시** + +```http +PUT /api/modules/sirsoft-board/admin/settings HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "_tab": "basic_defaults", + "notifications": [ + "예시값" + ], + "basic_defaults": [ + "예시값" + ], + "report_policy": [ + "예시값" + ], + "report_permissions": [ + "예시값" + ], + "display": [ + "예시값" + ], + "spam_security": [ + "예시값" + ], + "seo": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| basic_defaults | object | `{"type":"basic","per_page":20,"per_page_mobile":15,"order…` | 저장 후 갱신된 기본 설정 카테고리. 게시판 타입, 페이지당 글 수(PC/모바일), 정렬 기준/방향, 댓글·답글 사용 여부와 깊이, 제목·내용·댓글 길이 제한, 파일 업로드 허용/용량/개수/확장자, 새 글 표시 시간, 게시판 기본 권한(default_board_permissions) 등을 포함합니다. | +| report_policy | object | `{"auto_hide_threshold":5,"auto_hide_target":"both","daily…` | 저장 후 갱신된 신고 정책 카테고리. 자동 숨김 임계치(auto_hide_threshold)와 대상(auto_hide_target: post/comment/both), 사용자별 일일 신고 한도, 신고 거부 누적 제한(횟수/기간), 관리자·작성자 신고 알림 발송 여부와 채널을 포함합니다. | +| spam_security | object | `{"post_cooldown_seconds":0,"comment_cooldown_seconds":0,"…` | 저장 후 갱신된 스팸·보안 카테고리. 글·댓글·신고 작성 사이의 도배 방지 쿨다운 시간(초)과 조회수 캐시 TTL을 포함합니다. | +| display | object | `{"date_display_format":"standard"}` | 저장 후 갱신된 표시 설정 카테고리. 날짜 표시 형식(date_display_format: standard 절대 표기 / relative 상대 표기)을 포함합니다. | +| seo | object | `{"meta_boards_title":"{site_name}","meta_boards_descripti…` | 저장 후 갱신된 SEO 메타 태그 설정 카테고리. 게시판 목록/개별 게시판/글 상세 페이지의 메타 제목·설명 템플릿과 각 페이지의 SEO 생성 활성화 여부(seo_boards, seo_board, seo_post_detail)를 포함합니다. | +| notifications | object | `{"channels":[{"id":"mail","is_active":true,"sort_order":0…` | 저장 후 갱신된 알림 채널 설정 카테고리. 각 채널(mail, database 등)의 식별자, 활성화 여부(is_active), 정렬 순서(sort_order)를 담은 channels 배열을 포함합니다. | +| report_permissions | object | `{"view_roles":[],"manage_roles":[]}` | 저장 후 동기화된 신고 관리 권한 역할. 신고 내역을 조회할 수 있는 역할(view_roles)과 신고를 처리·관리할 수 있는 역할(manage_roles)의 식별자 배열이며, 요청에 포함된 경우 DB 권한 역할로 동기화되어 반환됩니다. | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-board::messages.settings.save_success", + "data": { + "basic_defaults": { + "type": "basic", + "per_page": 20, + "per_page_mobile": 15, + "order_by": "created_at", + "order_direction": "DESC", + "secret_mode": "{MASKED}", + "use_comment": true, + "use_reply": true, + "max_reply_depth": 5, + "max_comment_depth": 10, + "comment_order": "ASC", + "show_view_count": true, + "use_report": false, + "min_title_length": 2, + "max_title_length": 200, + "min_content_length": 2, + "max_content_length": 10000, + "min_comment_length": 2, + "max_comment_length": 1000, + "use_file_upload": false, + "max_file_size": 10, + "max_file_count": 5, + "allowed_extensions": [ + "jpg", + "jpeg", + "png", + "gif", + "webp", + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "ppt", + "pptx", + "hwp", + "txt", + "zip" + ], + "notify_admin_on_post": true, + "notify_author": true, + "new_display_hours": 24, + "blocked_keywords": [ + "시발", + "씨발", + "ㅅㅂ", + "ㅆㅂ", + "좆", + "ㅈㄴ", + "존나", + "씹", + "지랄", + "ㅈㄹ", + "개새끼", + "새끼", + "ㅅㄲ", + "개자식", + "병신", + "ㅂㅅ", + "미친놈", + "미친년", + "느금마", + "니애미", + "느금", + "엠창", + "엠뒤", + "빡대가리", + "개같은", + "개년", + "개놈", + "개돼지", + "개씹", + "개쓰레기", + "닥쳐", + "꺼져", + "죽어", + "자살해", + "뒤져", + "저능아", + "정신병자", + "썅", + "쌍놈", + "쌍년", + "쉬발", + "쒸발", + "시팔", + "씨팔", + "한남", + "한녀", + "김치녀", + "된장녀", + "맘충", + "틀딱", + "느개비", + "장애인놈", + "병신새끼", + "흑형", + "짱깨", + "쪽바리", + "왜놈", + "튀기", + "똥남아", + "야동", + "포르노", + "섹스", + "야사", + "음란", + "성인사이트", + "조건만남", + "원나잇", + "섹파", + "오피", + "키스방", + "매춘", + "성매매", + "몸파는" + ], + "default_board_permissions": { + "admin.posts.read": [ + "admin" + ], + "admin.posts.write": [ + "admin" + ], + "admin.posts.read-secret": [ + "admin" + ], + "admin.comments.read": [ + "admin" + ], + "admin.comments.write": [ + "admin" + ], + "admin.attachments.upload": [ + "admin" + ], + "admin.attachments.download": [ + "admin" + ], + "admin.manage": [ + "admin" + ], + "posts.read": [ + "admin", + "user", + "guest" + ], + "posts.write": [ + "admin", + "user" + ], + "posts.read-secret": [ + "admin" + ], + "comments.read": [ + "admin", + "user", + "guest" + ], + "comments.write": [ + "admin", + "user" + ], + "attachments.upload": [ + "admin", + "user" + ], + "attachments.download": [ + "admin", + "user", + "guest" + ], + "manager": [ + "admin" + ] + } + }, + "report_policy": { + "auto_hide_threshold": 5, + "auto_hide_target": "both", + "daily_report_limit": 10, + "rejection_limit_count": 5, + "rejection_limit_days": 30, + "notify_admin_on_report": true, + "notify_admin_on_report_scope": "per_case", + "notify_admin_on_report_channels": [ + "mail", + "database" + ], + "notify_author_on_report_action": true, + "notify_author_on_report_action_channels": [ + "mail", + "database" + ] + }, + "spam_security": { + "post_cooldown_seconds": 0, + "comment_cooldown_seconds": 0, + "report_cooldown_seconds": 60, + "view_count_cache_ttl": 86400 + }, + "display": { + "date_display_format": "standard" + }, + "seo": { + "meta_boards_title": "{site_name}", + "meta_boards_description": "", + "meta_board_title": "{board_name}", + "meta_board_description": "{board_description}", + "meta_post_title": "{board_name} - {post_title}", + "meta_post_description": "", + "seo_boards": true, + "seo_board": true, + "seo_post_detail": true + }, + "notifications": { + "channels": [ + { + "id": "mail", + "is_active": true, + "sort_order": 0 + }, + { + "id": "database", + "is_active": true, + "sort_order": 1 + } + ] + }, + "report_permissions": { + "view_roles": [], + "manage_roles": [] + } + } +} +``` **에러 응답** @@ -106,10 +694,37 @@ _단건 응답: `data` 객체의 필드._ | board_ids | body | array | 아니오 | — | board 식별자 배열 | | override_values | body | array | 아니오 | — | 환경설정 기본값 대신 사용할 재정의 값 맵. 지정한 필드에 대해 기본값이 아닌 임의의 값으로 일괄 적용할 때 사용합니다. | +**요청 예시** + +```http +POST /api/modules/sirsoft-board/admin/settings/bulk-apply HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "fields": [ + "예시값" + ], + "apply_all": true, + "board_ids": [ + "예시값" + ], + "override_values": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -133,10 +748,23 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/modules/sirsoft-board/admin/settings/clear-cache HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -161,10 +789,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | category | path | string | 예 | — | 분류 필터 (해당 분류의 항목만 조회) | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/admin/settings/{category} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-board/docs/api/users.md b/modules/_bundled/sirsoft-board/docs/api/users.md index 951db707..dd2b6953 100644 --- a/modules/_bundled/sirsoft-board/docs/api/users.md +++ b/modules/_bundled/sirsoft-board/docs/api/users.md @@ -29,6 +29,15 @@ | --- | --- | --- | --- | --- | --- | | user | path | string | 예 | — | 대상 user의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/users/a234c2b1-cde8-437f-b28b-23323be2b98d/posts HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -49,6 +58,69 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | | content_plain | string | `API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.` | 게시글 본문의 순수 텍스트입니다. HTML 모드 글은 태그를 제거한 평문으로 변환되며, 목록 미리보기용으로 사용합니다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "게시글 목록을 조회했습니다.", + "data": { + "current_page": 1, + "data": [ + { + "id": 1, + "board_slug": "apidoc-sample-board", + "board_name": "API 문서 샘플 게시판", + "activity_type": "authored", + "activity_count": 0, + "title": "API 문서 샘플 게시글", + "is_secret": "{MASKED}", + "status": "published", + "view_count": 44, + "comment_count": 0, + "created_at": "2026-07-08 10:41:34", + "created_at_formatted": "4시간 전", + "content_plain": "API 레퍼런스 실측용 완전 샘플 게시글 본문입니다." + } + ], + "first_page_url": "https://api.example.com/api/modules/sirsoft-board/users/a234c2b1-cde8-437f-b28b-23323be2b98d/posts?page=1", + "from": 1, + "last_page": 1, + "last_page_url": "https://api.example.com/api/modules/sirsoft-board/users/a234c2b1-cde8-437f-b28b-23323be2b98d/posts?page=1", + "links": [ + { + "url": null, + "label": "pagination.previous", + "page": null, + "active": false + }, + { + "url": "https://api.example.com/api/modules/sirsoft-board/users/a234c2b1-cde8-437f-b28b-23323be2b98d/posts?page=1", + "label": "1", + "page": 1, + "active": true + }, + { + "url": null, + "label": "pagination.next", + "page": null, + "active": false + } + ], + "next_page_url": null, + "path": "https://api.example.com/api/modules/sirsoft-board/users/a234c2b1-cde8-437f-b28b-23323be2b98d/posts", + "per_page": 25, + "prev_page_url": null, + "to": 1, + "total": 1 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -72,6 +144,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | user | path | string | 예 | — | 대상 user의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-board/users/a234c2b1-cde8-437f-b28b-23323be2b98d/posts/stats HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -81,6 +162,23 @@ _단건 응답: `data` 객체의 필드._ | posts_count | integer | `1` | posts 개수 (집계) | | comments_count | integer | `1` | comments 개수 (집계) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "성공적으로 처리되었습니다.", + "data": { + "posts_count": 1, + "comments_count": 1 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/addresses.md b/modules/_bundled/sirsoft-ecommerce/docs/api/addresses.md index 1a569a83..dd125472 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/addresses.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/addresses.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/addresses HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -35,6 +44,59 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | addresses | object | `{"data":[{"id":263,"user_id":"a1e0a91a-fba6-491c-a53e-728…` | 회원 본인 소유 배송지 컬렉션 (`data[]` 배송지 항목 배열 + `abilities.can_create` — UserAddressCollection 파생) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송지 목록을 조회했습니다.", + "data": { + "addresses": { + "data": [ + { + "id": 1, + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 배송지", + "recipient_name": "원진수", + "recipient_phone": "010-4988-7051", + "country_code": "KR", + "country_name": { + "ko": "한국", + "en": "South Korea" + }, + "zipcode": "27058", + "address": "충청남도 남양주시 테헤란로 6114", + "address_detail": "Quidem natus iste.", + "address_line_1": null, + "address_line_2": null, + "city": null, + "state": null, + "postal_code": null, + "is_default": false, + "is_domestic": true, + "is_international": false, + "full_address": "충청남도 남양주시 테헤란로 6114 Quidem natus iste.", + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "abilities": { + "can_update": true, + "can_delete": true, + "can_set_default": true + } + } + ], + "abilities": { + "can_create": true + } + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -76,9 +138,89 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.user_address.store_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/user/addresses HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": "예시 이름", + "recipient_name": "예시 이름", + "recipient_phone": "010-1234-5678", + "country_code": "KR", + "zipcode": "06234", + "province_code": "예시값", + "city": "예시값", + "address": "서울특별시 강남구 테헤란로 1", + "address_detail": "서울특별시 강남구 테헤란로 1", + "address_type_code": "R", + "address_line_1": "서울특별시 강남구 테헤란로 1", + "address_line_2": "서울특별시 강남구 테헤란로 1", + "intl_city": "예시값", + "intl_state": "예시값", + "intl_postal_code": "06234", + "is_default": true, + "force_overwrite": true +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| address | object | `{"id":9,"user_id":"a234c2b1-cde8-437f-b28b-23323be2b98d",…` | 기본주소 (국내) | + +**응답 예시** + +```http +HTTP/1.1 201 +``` + +```json +{ + "success": true, + "message": "배송지가 추가되었습니다.", + "data": { + "address": { + "id": 9, + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "실측 예시값", + "recipient_name": "실측 예시값", + "recipient_phone": "실측 예시값", + "country_code": "KR", + "country_name": { + "ko": "한국", + "en": "South Korea" + }, + "zipcode": "실측 예시값", + "address": "실측 예시값", + "address_detail": "실측 예시값", + "address_line_1": null, + "address_line_2": null, + "city": "실측 예시값", + "state": null, + "postal_code": null, + "is_default": true, + "is_domestic": true, + "is_international": false, + "full_address": "실측 예시값 실측 예시값", + "created_at": "2026-07-08 15:00:36", + "updated_at": "2026-07-08 15:00:36", + "abilities": { + "can_update": true, + "can_delete": false, + "can_set_default": false + } + } + } +} +``` **에러 응답** @@ -104,9 +246,34 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/user/addresses/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송지가 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -132,9 +299,68 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/addresses/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| address | object | `{"id":1,"user_id":"a234c2b1-cde8-437f-b28b-23323be2b98d",…` | 기본주소 (국내) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송지 정보를 조회했습니다.", + "data": { + "address": { + "id": 1, + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 배송지", + "recipient_name": "원진수", + "recipient_phone": "010-4988-7051", + "country_code": "KR", + "country_name": { + "ko": "한국", + "en": "South Korea" + }, + "zipcode": "27058", + "address": "충청남도 남양주시 테헤란로 6114", + "address_detail": "Quidem natus iste.", + "address_line_1": null, + "address_line_2": null, + "city": null, + "state": null, + "postal_code": null, + "is_default": false, + "is_domestic": true, + "is_international": false, + "full_address": "충청남도 남양주시 테헤란로 6114 Quidem natus iste.", + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "abilities": { + "can_update": true, + "can_delete": true, + "can_set_default": true + } + } + } +} +``` **에러 응답** @@ -178,9 +404,88 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.user_address.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/user/addresses/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": "예시 이름", + "recipient_name": "예시 이름", + "recipient_phone": "010-1234-5678", + "country_code": "KR", + "zipcode": "06234", + "province_code": "예시값", + "city": "예시값", + "address": "서울특별시 강남구 테헤란로 1", + "address_detail": "서울특별시 강남구 테헤란로 1", + "address_type_code": "R", + "address_line_1": "서울특별시 강남구 테헤란로 1", + "address_line_2": "서울특별시 강남구 테헤란로 1", + "intl_city": "예시값", + "intl_state": "예시값", + "intl_postal_code": "06234", + "is_default": true +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| address | object | `{"id":1,"user_id":"a234c2b1-cde8-437f-b28b-23323be2b98d",…` | 기본주소 (국내) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송지가 수정되었습니다.", + "data": { + "address": { + "id": 1, + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "실측 예시값", + "recipient_name": "실측 예시값", + "recipient_phone": "실측 예시값", + "country_code": "KR", + "country_name": { + "ko": "한국", + "en": "South Korea" + }, + "zipcode": "실측 예시값", + "address": "실측 예시값", + "address_detail": "실측 예시값", + "address_line_1": null, + "address_line_2": null, + "city": "실측 예시값", + "state": null, + "postal_code": null, + "is_default": true, + "is_domestic": true, + "is_international": false, + "full_address": "실측 예시값 실측 예시값", + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:00:36", + "abilities": { + "can_update": true, + "can_delete": false, + "can_set_default": false + } + } + } +} +``` **에러 응답** @@ -207,9 +512,34 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/user/addresses/1/default HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "기본 배송지로 설정되었습니다.", + "data": null +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md b/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md index 1f62a7fc..d26992e3 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md @@ -34,6 +34,15 @@ | sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | | locale | query | string | 아니오 | `ko`, `en`, `fr`, `ja` | 로케일 코드 (표시 언어/지역) | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/brands?is_active=1&search=%EC%98%88%EC%8B%9C%EA%B0%92&sort=name_asc&sort_by=name&sort_order=asc&locale=ko HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -57,6 +66,53 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | products_count | integer | `0` | products 개수 (집계) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "브랜드 목록을 조회했습니다.", + "data": { + "data": [ + { + "number": 1, + "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", + "creator": [], + "updater": [], + "products_count": 0, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -88,10 +144,34 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.brand.create_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/brands HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "slug": "example-key", + "website": "예시값", + "sort_order": 1, + "is_active": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -117,9 +197,40 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | brand | path | string | 예 | — | 대상 brand의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/brands/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| brand_id | integer | `1` | brand 식별자 (연관 리소스 참조) | +| products_count | integer | `0` | products 개수 (집계) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "브랜드가 삭제되었습니다.", + "data": { + "brand_id": 1, + "products_count": 0 + } +} +``` **에러 응답** @@ -153,10 +264,34 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.brand.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/brands/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "slug": "example-key", + "website": "예시값", + "sort_order": 1, + "is_active": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -183,9 +318,69 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/brands/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| 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 | null | `null` | 브랜드 웹사이트 URL | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| is_active | boolean | `true` | active 여부 | +| icon | string | `tag` | 아이콘 식별자 (아이콘 클래스/이름) | +| created_at | string | `2026-07-08 10:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 | +| products_count | integer | `0` | products 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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": 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 + } + } +} +``` **에러 응답** @@ -212,9 +407,69 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/brands/1/toggle-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| 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 | null | `null` | 브랜드 웹사이트 URL | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| is_active | boolean | `false` | active 여부 | +| icon | string | `tag` | 아이콘 식별자 (아이콘 클래스/이름) | +| created_at | string | `2026-07-08 10:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 15:00:16` | 최종 수정 일시 | +| products_count | integer | `0` | products 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-ecommerce::messages.brands.status_changed", + "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": false, + "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 + } + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/cart.md b/modules/_bundled/sirsoft-ecommerce/docs/api/cart.md index a0df9a01..bbfd7d86 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/cart.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/cart.md @@ -31,10 +31,23 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.cart.delete_items_validation_rules`). +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/cart?ids=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -60,6 +73,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.cart.get_validation_rules`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/cart?selected_ids=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -73,6 +95,81 @@ _단건 응답: `data` 객체의 필드._ | has_unshippable_items | boolean | `false` | unshippable items 여부 | | selected_shipping_country | string | `KR` | 배송비 계산에 적용된 배송 국가 코드 (ResolveShippingCountry 해석 결과) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "장바구니 목록을 조회했습니다.", + "data": { + "items": [], + "item_ids": [], + "item_count": 0, + "calculation": { + "items": [], + "summary": { + "subtotal": 0, + "subtotal_formatted": "0원", + "product_coupon_discount": 0, + "product_coupon_discount_formatted": "0원", + "code_discount": 0, + "code_discount_formatted": "0원", + "order_coupon_discount": 0, + "order_coupon_discount_formatted": "0원", + "total_coupon_discount": 0, + "total_coupon_discount_formatted": "0원", + "total_discount": 0, + "discount_formatted": "0원", + "base_shipping_total": 0, + "extra_shipping_total": 0, + "total_shipping": 0, + "total_shipping_fee": 0, + "shipping_fee_formatted": "0원", + "shipping_discount": 0, + "shipping_discount_formatted": "0원", + "taxable_amount": 0, + "tax_free_amount": 0, + "points_earning": 0, + "total_mileage": 0, + "mileage_formatted": "0P", + "points_used": 0, + "points_used_formatted": "0원", + "payment_amount": 0, + "payment_amount_formatted": "0원", + "final_amount": 0, + "final_amount_formatted": "0원", + "coupon_discount": 0, + "coupon_discount_formatted": "0원", + "order_discount": 0, + "order_discount_formatted": "0원" + }, + "promotions": { + "coupon_issue_ids": [], + "item_coupons": [], + "discount_code": null, + "product_promotions": { + "coupons": [], + "discount_codes": [], + "events": [] + }, + "order_promotions": { + "coupons": [], + "discount_codes": [], + "events": [] + } + }, + "validation_errors": [] + }, + "has_unshippable_items": false, + "selected_shipping_country": "KR" + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -99,10 +196,31 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.cart.bulk_add_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/cart HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +Content-Type: application/json + +{ + "product_id": 1, + "items": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -124,9 +242,38 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/cart/all HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| deleted_count | integer | `0` | deleted 개수 (집계) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "장바구니가 비워졌습니다. 0개 상품이 삭제되었습니다.", + "data": { + "deleted_count": 0 + } +} +``` **에러 응답** @@ -151,6 +298,15 @@ _대표 에러 없음 (공개 조회). +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| cart_key | string | `ck_QBQT80EINHHXLO8HYHntMpmUZrPujPka` | 비회원 장바구니 식별 키 (`ck_` + 32자 영숫자. 이후 요청의 `X-Cart-Key` 헤더에 실어 자신의 장바구니를 식별) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "장바구니 키가 발급되었습니다.", + "data": { + "cart_key": "ck_QBQT80EINHHXLO8HYHntMpmUZrPujPka" + } +} +``` **에러 응답** @@ -203,10 +404,23 @@ _대표 에러 없음 (공개 조회). +**응답 예시** + + + **에러 응답** _대표 에러 없음 (공개 조회). _ @@ -230,9 +444,109 @@ _대표 에러 없음 (공개 조회). +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| items | array | `[]` | 장바구니 라인 아이템 목록 (상품·옵션·수량 등 — CartItemResource 파생) | +| item_ids | array | `[]` | item 식별자 배열 (연관 리소스 참조) | +| item_count | integer | `0` | item 개수 (집계) | +| calculation | object | `{"items":[],"summary":{"subtotal":0,"subtotal_formatted":…` | 선택 아이템 기준 금액 계산 결과 (상품 소계·할인·배송비 등 — OrderCalculationResult 파생) | +| has_unshippable_items | boolean | `false` | unshippable items 여부 | +| selected_shipping_country | string | `KR` | 배송비 계산에 적용된 배송 국가 코드 (ResolveShippingCountry 해석 결과) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "장바구니 목록을 조회했습니다.", + "data": { + "items": [], + "item_ids": [], + "item_count": 0, + "calculation": { + "items": [], + "summary": { + "subtotal": 0, + "subtotal_formatted": "0원", + "product_coupon_discount": 0, + "product_coupon_discount_formatted": "0원", + "code_discount": 0, + "code_discount_formatted": "0원", + "order_coupon_discount": 0, + "order_coupon_discount_formatted": "0원", + "total_coupon_discount": 0, + "total_coupon_discount_formatted": "0원", + "total_discount": 0, + "discount_formatted": "0원", + "base_shipping_total": 0, + "extra_shipping_total": 0, + "total_shipping": 0, + "total_shipping_fee": 0, + "shipping_fee_formatted": "0원", + "shipping_discount": 0, + "shipping_discount_formatted": "0원", + "taxable_amount": 0, + "tax_free_amount": 0, + "points_earning": 0, + "total_mileage": 0, + "mileage_formatted": "0P", + "points_used": 0, + "points_used_formatted": "0원", + "payment_amount": 0, + "payment_amount_formatted": "0원", + "final_amount": 0, + "final_amount_formatted": "0원", + "coupon_discount": 0, + "coupon_discount_formatted": "0원", + "order_discount": 0, + "order_discount_formatted": "0원" + }, + "promotions": { + "coupon_issue_ids": [], + "item_coupons": [], + "discount_code": null, + "product_promotions": { + "coupons": [], + "discount_codes": [], + "events": [] + }, + "order_promotions": { + "coupons": [], + "discount_codes": [], + "events": [] + } + }, + "validation_errors": [] + }, + "has_unshippable_items": false, + "selected_shipping_country": "KR" + } +} +``` **에러 응답** @@ -257,10 +571,23 @@ _대표 에러 없음 (공개 조회). +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -289,10 +616,32 @@ _대표 에러 없음 (공개 조회). +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -320,10 +669,28 @@ _대표 에러 없음 (공개 조회). +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/categories.md b/modules/_bundled/sirsoft-ecommerce/docs/api/categories.md index bbc75ea7..098a31c3 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/categories.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/categories.md @@ -34,6 +34,15 @@ | flat | query | boolean | 아니오 | — | true 면 깊이 들여쓰기를 포함한 평면 리스트로 반환 (TagInput 등에 사용) | | max_depth | query | integer | 아니오 | min 1, max 10 | 조회할 최대 계층 깊이 제한 (1~10) | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/categories?parent_id=%EC%98%88%EC%8B%9C%EA%B0%92&is_active=1&search=%EC%98%88%EC%8B%9C%EA%B0%92&hierarchical=1&flat=1&max_depth=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -61,6 +70,57 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | children_count | integer | `0` | children 개수 (집계) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "카테고리 목록을 조회했습니다.", + "data": { + "data": [ + { + "id": 1, + "name": { + "ko": "API 문서 샘플 카테고리", + "en": "API Doc Sample Category" + }, + "description": null, + "localized_name": "API 문서 샘플 카테고리", + "parent_id": null, + "path": "0", + "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 + } + } + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -95,10 +155,39 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category.create_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/categories HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "description": [ + "예시 내용입니다." + ], + "parent_id": "예시값", + "slug": "example-key", + "is_active": true, + "meta_title": "예시 제목", + "meta_description": "예시 내용입니다.", + "temp_key": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -129,10 +218,43 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category-image.filter_upload_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/categories/images HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary +Content-Disposition: form-data; name="temp_key" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="collection" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="alt_text" + +예시값 +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -160,10 +282,30 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category-image.filter_reorder_validation_rules`). +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/categories/images/reorder HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "order": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -189,10 +331,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/categories/images/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -221,10 +376,33 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category.reorder_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/categories/order HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "parent_menus": [ + "예시값" + ], + "child_menus": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -248,6 +426,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/categories/tree HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -277,6 +464,59 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | children_count | integer | `2` | children 개수 (집계) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "카테고리 목록을 조회했습니다.", + "data": { + "data": [ + { + "id": 1, + "name": { + "ko": "API 문서 샘플 카테고리", + "en": "API Doc Sample Category" + }, + "description": null, + "localized_name": "API 문서 샘플 카테고리", + "parent_id": null, + "path": "0", + "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", + "parent": null, + "children": [], + "images": [], + "products_count": 0, + "children_count": 0, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -307,10 +547,43 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category-image.filter_upload_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/categories/{categoryId}/images HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary +Content-Disposition: form-data; name="temp_key" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="collection" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="alt_text" + +예시값 +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -337,9 +610,38 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | category | path | string | 예 | — | 분류 필터 (해당 분류의 항목만 조회) | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/categories/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| category_id | integer | `1` | category 식별자 (연관 리소스 참조) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "카테고리가 삭제되었습니다.", + "data": { + "category_id": 1 + } +} +``` **에러 응답** @@ -376,10 +678,39 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/categories/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "description": [ + "예시 내용입니다." + ], + "parent_id": "예시값", + "slug": "example-key", + "is_active": true, + "meta_title": "예시 제목", + "meta_description": "예시 내용입니다.", + "temp_key": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -406,9 +737,87 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/categories/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 카테고리","en":"API Doc Sample Category"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| description | null | `null` | 설명 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `API 문서 샘플 카테고리` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| path | string | `0` | Materialized Path: 1/5/23 | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| is_active | boolean | `true` | active 여부 | +| slug | string | `apidoc-sample-category` | URL 친화 식별자 (slug) | +| url | string | `apidoc-sample-category` | SortableMenuItem 표시용 URL (slug 값을 그대로 사용) | +| icon | string | `folder` | 아이콘 식별자 (아이콘 클래스/이름) | +| meta_title | null | `null` | SEO 제목 (다국어 JSON) | +| meta_description | null | `null` | SEO 설명 (다국어 JSON) | +| created_at | string | `2026-07-08 01:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 01:44:49` | 최종 수정 일시 | +| parent | null | `null` | 상위 항목 객체 (parent 관계 파생) | +| children | array | `[]` | 하위 항목 배열 (계층 트리 — children 관계 파생) | +| images | array | `[]` | 카테고리 이미지 배열 (images 관계 로드 시 — id/hash/download_url/alt_text 등) | +| products_count | integer | `0` | products 개수 (집계) | +| children_count | integer | `0` | children 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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": "0", + "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", + "parent": null, + "children": [], + "images": [], + "products_count": 0, + "children_count": 0, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -435,9 +844,83 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/categories/1/toggle-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 카테고리","en":"API Doc Sample Category"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| description | null | `null` | 설명 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `API 문서 샘플 카테고리` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| path | string | `0` | Materialized Path: 1/5/23 | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| is_active | boolean | `false` | active 여부 | +| slug | string | `apidoc-sample-category` | URL 친화 식별자 (slug) | +| url | string | `apidoc-sample-category` | SortableMenuItem 표시용 URL (slug 값을 그대로 사용) | +| icon | string | `folder` | 아이콘 식별자 (아이콘 클래스/이름) | +| meta_title | null | `null` | SEO 제목 (다국어 JSON) | +| meta_description | null | `null` | SEO 설명 (다국어 JSON) | +| created_at | string | `2026-07-08 01:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 06:00:17` | 최종 수정 일시 | +| images | array | `[]` | 카테고리 이미지 배열 (images 관계 로드 시 — id/hash/download_url/alt_text 등) | +| products_count | integer | `0` | products 개수 (집계) | +| children_count | integer | `0` | children 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-ecommerce::messages.categories.status_changed", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 카테고리", + "en": "API Doc Sample Category" + }, + "description": null, + "localized_name": "API 문서 샘플 카테고리", + "parent_id": null, + "path": "0", + "depth": 0, + "sort_order": 0, + "is_active": false, + "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 + } + } +} +``` **에러 응답** @@ -462,6 +945,14 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/categories HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -475,6 +966,34 @@ _요청 파라미터 없음._ | products_count | integer | `22` | products 개수 (집계) | | children | array | `[{"id":88,"name":{"ko":"남성","en":"Men"},"name_localized":…` | 하위 항목 배열 (계층 트리 — children 관계 파생) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "카테고리 목록을 조회했습니다.", + "data": [ + { + "id": 1, + "name": { + "ko": "API 문서 샘플 카테고리", + "en": "API Doc Sample Category" + }, + "name_localized": "API 문서 샘플 카테고리", + "slug": "apidoc-sample-category", + "depth": 0, + "parent_id": null, + "products_count": 0, + "children": [] + } + ] +} +``` + **에러 응답** _대표 에러 없음 (공개 조회). _ @@ -496,9 +1015,68 @@ _대표 에러 없음 (공개 조회). +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 카테고리","en":"API Doc Sample Category"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| name_localized | string | `API 문서 샘플 카테고리` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| description | null | `null` | 설명 (다국어 필드는 로케일별 값 객체) | +| description_localized | null | `null` | `description` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| slug | string | `apidoc-sample-category` | URL 친화 식별자 (slug) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| products_count | integer | `0` | products 개수 (집계) | +| breadcrumb | array | `[{"id":1,"name":"API 문서 샘플 카테고리","slug":"apidoc-sample-ca…` | 최상위부터 현재 카테고리까지의 상위 경로 배열 (각 항목에 id·현지화 name·slug — 스토어프론트 breadcrumb 표시용) | +| images | array | `[]` | 카테고리 이미지 배열 (images 관계 로드 시 — id/hash/download_url/alt_text 등) | +| children | array | `[]` | 하위 항목 배열 (계층 트리 — children 관계 파생) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-ecommerce::messages.categories.fetch_success", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 카테고리", + "en": "API Doc Sample Category" + }, + "name_localized": "API 문서 샘플 카테고리", + "description": null, + "description_localized": null, + "slug": "apidoc-sample-category", + "depth": 0, + "parent_id": null, + "products_count": 0, + "breadcrumb": [ + { + "id": 1, + "name": "API 문서 샘플 카테고리", + "slug": "apidoc-sample-category" + } + ], + "images": [], + "children": [] + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/category-image.md b/modules/_bundled/sirsoft-ecommerce/docs/api/category-image.md index 3217d16b..32468e36 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/category-image.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/category-image.md @@ -29,10 +29,22 @@ | --- | --- | --- | --- | --- | --- | | hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/category-image/{hash} HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/checkout.md b/modules/_bundled/sirsoft-ecommerce/docs/api/checkout.md index 881f88cd..93186507 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/checkout.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/checkout.md @@ -27,10 +27,23 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/checkout HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** _대표 에러 없음 (공개 조회). _ @@ -56,10 +69,23 @@ _대표 에러 없음 (공개 조회). +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -88,10 +114,37 @@ _대표 에러 없음 (공개 조회). +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -123,10 +176,36 @@ _대표 에러 없음 (공개 조회). +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -148,10 +227,23 @@ _대표 에러 없음 (공개 조회). +**응답 예시** + + + **에러 응답** _대표 에러 없음 (공개 조회). _ diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/claim-reasons.md b/modules/_bundled/sirsoft-ecommerce/docs/api/claim-reasons.md index 00616675..c6568004 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/claim-reasons.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/claim-reasons.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/claim-reasons HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -50,6 +59,79 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | updater | array | `[]` | 최종 수정자 정보 객체 (id/name — updater 관계 로드 시) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "클래임 사유 목록을 조회했습니다.", + "data": { + "data": [ + { + "number": 8, + "id": 1, + "type": "refund", + "code": "order_mistake", + "name": { + "ko": "주문 실수", + "en": "Order Mistake" + }, + "localized_name": "주문 실수", + "fault_type": "customer", + "fault_type_label": "고객 귀책", + "is_user_selectable": true, + "is_active": true, + "sort_order": 0, + "created_at": "2026-07-08 10:43:32", + "updated_at": "2026-07-08 10:43:32", + "creator": [], + "updater": [], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + }, + { + "number": 7, + "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", + "creator": [], + "updater": [], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + }, + "... (총 8건 중 2건 표시)" + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -80,10 +162,36 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | | sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/claim-reasons HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "type": "예시값", + "code": "예시값", + "name": [ + "예시 이름" + ], + "fault_type": "예시값", + "is_user_selectable": true, + "is_active": true, + "sort_order": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -107,6 +215,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/claim-reasons/active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -130,6 +247,79 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | updater | array | `[]` | 최종 수정자 정보 객체 (id/name — updater 관계 로드 시) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "클래임 사유 목록을 조회했습니다.", + "data": { + "data": [ + { + "number": 8, + "id": 1, + "type": "refund", + "code": "order_mistake", + "name": { + "ko": "주문 실수", + "en": "Order Mistake" + }, + "localized_name": "주문 실수", + "fault_type": "customer", + "fault_type_label": "고객 귀책", + "is_user_selectable": true, + "is_active": true, + "sort_order": 0, + "created_at": "2026-07-08 10:43:32", + "updated_at": "2026-07-08 10:43:32", + "creator": [], + "updater": [], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + }, + { + "number": 7, + "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", + "creator": [], + "updater": [], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + }, + "... (총 8건 중 2건 표시)" + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -154,9 +344,38 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/claim-reasons/8 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| reason_id | integer | `8` | reason 식별자 (연관 리소스 참조) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "클래임 사유가 삭제되었습니다.", + "data": { + "reason_id": 8 + } +} +``` **에러 응답** @@ -183,9 +402,69 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/claim-reasons/8 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `8` | 기본 키 (내부 식별자) | +| type | string | `refund` | 사유 유형 (refund, exchange, return 등) | +| code | string | `apidoc_sample` | 고유 코드 (order_mistake 등) | +| name | object | `{"ko":"API 문서 샘플 사유","en":"API Doc Sample Reason"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `API 문서 샘플 사유` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| fault_type | string | `customer` | 귀책 구분 (customer, seller, carrier) | +| fault_type_label | string | `고객 귀책` | `fault_type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| is_user_selectable | boolean | `true` | user selectable 여부 | +| is_active | boolean | `true` | active 여부 | +| 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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" + }, + "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 + } + } +} +``` **에러 응답** @@ -219,10 +498,36 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | | sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/claim-reasons/8 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "type": "예시값", + "code": "예시값", + "name": [ + "예시 이름" + ], + "fault_type": "예시값", + "is_user_selectable": true, + "is_active": true, + "sort_order": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -249,9 +554,69 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/claim-reasons/8/toggle-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `8` | 기본 키 (내부 식별자) | +| type | string | `refund` | 사유 유형 (refund, exchange, return 등) | +| code | string | `apidoc_sample` | 고유 코드 (order_mistake 등) | +| name | object | `{"ko":"API 문서 샘플 사유","en":"API Doc Sample Reason"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `API 문서 샘플 사유` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| fault_type | string | `customer` | 귀책 구분 (customer, seller, carrier) | +| fault_type_label | string | `고객 귀책` | `fault_type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| is_user_selectable | boolean | `true` | user selectable 여부 | +| is_active | boolean | `false` | active 여부 | +| 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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" + }, + "localized_name": "API 문서 샘플 사유", + "fault_type": "customer", + "fault_type_label": "고객 귀책", + "is_user_selectable": true, + "is_active": false, + "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 + } + } +} +``` **에러 응답** @@ -276,6 +641,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/claim-reasons HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -299,6 +673,79 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | updater | array | `[]` | 최종 수정자 정보 객체 (id/name — updater 관계 로드 시) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "클래임 사유 목록을 조회했습니다.", + "data": { + "data": [ + { + "number": 7, + "id": 1, + "type": "refund", + "code": "order_mistake", + "name": { + "ko": "주문 실수", + "en": "Order Mistake" + }, + "localized_name": "주문 실수", + "fault_type": "customer", + "fault_type_label": "고객 귀책", + "is_user_selectable": true, + "is_active": true, + "sort_order": 0, + "created_at": "2026-07-08 10:43:32", + "updated_at": "2026-07-08 10:43:32", + "creator": [], + "updater": [], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + }, + { + "number": 6, + "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", + "creator": [], + "updater": [], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + }, + "... (총 7건 중 2건 표시)" + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/coupons.md b/modules/_bundled/sirsoft-ecommerce/docs/api/coupons.md index 32d268bc..e1abac71 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/coupons.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/coupons.md @@ -32,6 +32,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.coupon.user_list_validation_rules`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/coupons?status=available&per_page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -40,6 +49,33 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | coupons | object | `{"data":[{"id":8611,"coupon_id":156,"user_id":"a1e0a91a-f…` | 회원이 발급받은 쿠폰(발급 내역) 페이지네이션 객체 (`data[]` 발급 건 + `pagination` — CouponIssueCollection 직렬화, 쿠폰 정의가 아닌 회원별 발급 건) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "쿠폰 목록을 조회했습니다.", + "data": { + "coupons": { + "data": [], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 0, + "from": null, + "to": null, + "has_more_pages": false + } + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -66,6 +102,15 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.coupon.user_available_validation_rules`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/coupons/available?product_ids=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -74,6 +119,22 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | coupons | array | `[{"id":4702,"coupon_id":106,"user_id":1,"coupon_code":nul…` | 현재 장바구니 상품에 적용 가능한 보유 쿠폰(발급 건) 배열 (상품/카테고리 범위·최소 주문금액·유효기간을 만족해 주문에 곧바로 선택 가능한 후보만) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "사용 가능한 쿠폰 목록을 조회했습니다.", + "data": { + "coupons": [] + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -100,6 +161,15 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.coupon.user_downloadable_validation_rules`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/coupons/downloadable?per_page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -146,6 +216,98 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | included_categories | array | `[]` | 적용 대상 포함 카테고리 목록 (target_scope=categories 시 이 카테고리에만 적용) | | excluded_categories | array | `[]` | 적용 제외 카테고리 목록 (해당 카테고리는 쿠폰 적용에서 제외) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "다운로드 가능한 쿠폰 목록을 불러왔습니다.", + "data": { + "current_page": 1, + "data": [ + { + "id": 1, + "name": { + "ko": "API 문서 샘플 쿠폰", + "en": "API Doc Sample Coupon" + }, + "description": null, + "target_type": "order_amount", + "discount_type": "fixed", + "discount_value": "1000.00", + "discount_max_amount": null, + "min_order_amount": "0.00", + "issue_method": "download", + "issue_condition": "manual", + "issue_status": "issuing", + "total_quantity": null, + "issued_count": 0, + "per_user_limit": 1, + "valid_type": "period", + "valid_days": null, + "valid_from": null, + "valid_to": null, + "issue_from": null, + "issue_to": null, + "is_combinable": false, + "target_scope": "all", + "created_by": null, + "created_at": "2026-07-08T01:44:49.000000Z", + "updated_at": "2026-07-08T01:44:49.000000Z", + "deleted_at": null, + "is_downloaded": false, + "user_issued_count": 0, + "coupon_id": 1, + "localized_name": "API 문서 샘플 쿠폰", + "target_type_short_label": "주문", + "valid_period_formatted": "-", + "min_order_amount_formatted": "0원", + "remaining_quantity": null, + "benefit_formatted": "1,000원 할인", + "included_products": [], + "excluded_products": [], + "included_categories": [], + "excluded_categories": [] + } + ], + "first_page_url": "https://api.example.com/api/modules/sirsoft-ecommerce/user/coupons/downloadable?page=1", + "from": 1, + "last_page": 1, + "last_page_url": "https://api.example.com/api/modules/sirsoft-ecommerce/user/coupons/downloadable?page=1", + "links": [ + { + "url": null, + "label": "pagination.previous", + "page": null, + "active": false + }, + { + "url": "https://api.example.com/api/modules/sirsoft-ecommerce/user/coupons/downloadable?page=1", + "label": "1", + "page": 1, + "active": true + }, + { + "url": null, + "label": "pagination.next", + "page": null, + "active": false + } + ], + "next_page_url": null, + "path": "https://api.example.com/api/modules/sirsoft-ecommerce/user/coupons/downloadable", + "per_page": 25, + "prev_page_url": null, + "to": 1, + "total": 1 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -171,10 +333,28 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | couponId | path | string | 예 | — | 대상 coupon의 식별자 | | coupon_id | body | integer | 예 | — | coupon 식별자 | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/user/coupons/{couponId}/download HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "coupon_id": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/currency.md b/modules/_bundled/sirsoft-ecommerce/docs/api/currency.md index 24784ecc..c471ab7c 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/currency.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/currency.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/currency HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -35,6 +44,22 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | preferred_currency | null | `null` | 회원이 저장한 선호 결제 통화 코드 (미설정 시 `null`) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "결제 통화를 조회했습니다.", + "data": { + "preferred_currency": null + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -58,10 +83,28 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | currency | body | string | 예 | — | 저장할 선호 결제 통화 코드 (등록 통화: is_default 또는 exchange_rate>0 인 통화만 허용) | +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/user/currency HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "currency": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/dashboard.md b/modules/_bundled/sirsoft-ecommerce/docs/api/dashboard.md index 11190efe..c9a97e48 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/dashboard.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/dashboard.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/dashboard/overview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -41,6 +50,28 @@ _단건 응답: `data` 객체의 필드._ | cancellations | integer | `0` | 오늘 취소 상태 주문상품 수량 (전체취소 기준, 부분취소 포함) | | returns | integer | `0` | 오늘 반품 상태 주문상품 수량 (환불 도메인 미반영으로 현재 항상 0) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "대시보드 데이터를 조회했습니다.", + "data": { + "pending_payment": 0, + "payment_complete": 0, + "preparing": 0, + "shipping_ready": 0, + "shipping": 0, + "cancellations": 0, + "returns": 0 + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -65,6 +96,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | limit | query | integer | 아니오 | min 1, max 50 | 반환할 최대 항목 수 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/dashboard/pending-inquiries?limit=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -75,6 +115,33 @@ _단건 응답: `data` 객체의 필드._ | total | integer | `1` | 전체 개수 (집계) | | board_slug | null | `null` | 문의가 저장된 연동 게시판의 slug (관리자 문의 상세 링크용, 미연동 시 null) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "대시보드 데이터를 조회했습니다.", + "data": { + "items": [ + { + "id": 1, + "product_id": 1, + "inquirable_id": 59404, + "product_name": "iste et inventore", + "author_name": "API 문서 샘플 사용자", + "created_at": "2026-07-08 10:44:49" + } + ], + "total": 1, + "board_slug": null + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -100,6 +167,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | limit | query | integer | 아니오 | min 1, max 50 | 반환할 최대 항목 수 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/dashboard/recent-reviews?limit=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -111,6 +187,29 @@ _단건 응답: `data` 객체의 필드._ | author_name | string | `API 문서 샘플 사용자` | 리뷰 작성자 이름 (user 관계 파생) | | created_at | string | `2026-07-07 14:47:31` | 생성 일시 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "대시보드 데이터를 조회했습니다.", + "data": [ + { + "id": 1, + "product_id": 1, + "product_name": "API 문서 샘플 상품", + "rating": 5, + "author_name": "API 문서 샘플 사용자", + "created_at": "2026-07-08 10:44:49" + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -134,6 +233,15 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/dashboard/sales-graph HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -148,6 +256,64 @@ _단건 응답: `data` 객체의 필드._ | updated_at | null | `null` | 최종 수정 일시 | | updated_at_display | string | `` | 집계 마지막 갱신 시각의 사용자 타임존 HH:mm 캡션 (갱신 이력 없으면 빈 문자열) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "대시보드 데이터를 조회했습니다.", + "data": { + "days": [ + { + "date": "2026-07-02", + "sales_quantity": 0, + "sales_amount": 0 + }, + { + "date": "2026-07-03", + "sales_quantity": 0, + "sales_amount": 0 + }, + { + "date": "2026-07-04", + "sales_quantity": 0, + "sales_amount": 0 + }, + { + "date": "2026-07-05", + "sales_quantity": 0, + "sales_amount": 0 + }, + { + "date": "2026-07-06", + "sales_quantity": 0, + "sales_amount": 0 + }, + { + "date": "2026-07-07", + "sales_quantity": 0, + "sales_amount": 0 + }, + { + "date": "2026-07-08", + "sales_quantity": 0, + "sales_amount": 0 + } + ], + "total_quantity": 0, + "total_sales": 0, + "quantity_change": null, + "sales_change": null, + "updated_at": null, + "updated_at_display": "" + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/extra-fee-templates.md b/modules/_bundled/sirsoft-ecommerce/docs/api/extra-fee-templates.md index a299a5c7..82062633 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/extra-fee-templates.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/extra-fee-templates.md @@ -35,6 +35,15 @@ | per_page | query | integer | 아니오 | min 10, max 100 | 페이지당 항목 수 | | page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/extra-fee-templates?search=%EC%98%88%EC%8B%9C%EA%B0%92®ion=%EC%98%88%EC%8B%9C%EA%B0%92&is_active=%2C%20&sort_by=id&sort_order=asc&per_page=1&page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -55,6 +64,62 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "추가배송비 템플릿 목록을 조회했습니다.", + "data": { + "data": [ + { + "number": 1, + "id": 1, + "zipcode": "00000", + "fee": 3000, + "fee_formatted": "3,000원", + "region": null, + "description": null, + "is_active": true, + "created_by": null, + "updated_by": null, + "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 + } + } + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + }, + "statistics": { + "total": 1, + "active": 1, + "inactive": 0, + "by_region": [] + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -84,10 +149,32 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | description | body | string | 아니오 | max 1000 | 설명 | | is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/extra-fee-templates HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "zipcode": "06234", + "fee": 1, + "region": "예시값", + "description": "예시 내용입니다.", + "is_active": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -111,6 +198,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/active-settings HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -119,6 +215,26 @@ _요청 파라미터 없음._ | fee | integer | `3000` | 해당 우편번호의 추가 배송비 (float 변환값) | | region | string | `` | 지역명 (없으면 빈 문자열) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "활성 추가배송비 템플릿 설정을 조회했습니다.", + "data": [ + { + "zipcode": "00000", + "fee": 3000, + "region": "" + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -143,10 +259,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | ids | query | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/bulk?ids=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -172,10 +301,30 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | items | body | array | 예 | min 1, max 1000 | 처리 대상 항목 배열 | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/bulk HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "items": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -202,10 +351,31 @@ _요청 파라미터 없음._ | ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | | is_active | body | boolean | 예 | — | 활성 여부 (true 활성 / false 비활성) | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/bulk-toggle-active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "is_active": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -231,9 +401,34 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "추가배송비 템플릿이 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -260,9 +455,64 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| zipcode | string | `00000` | 우편번호 (단일 또는 범위) | +| fee | integer | `3000` | 추가 배송비 | +| fee_formatted | string | `3,000원` | `fee` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| region | null | `null` | 지역명 (예: 제주도, 울릉도) | +| description | null | `null` | 설명 (다국어 필드는 로케일별 값 객체) | +| is_active | boolean | `true` | active 여부 | +| created_by | null | `null` | 생성자 ID | +| updated_by | null | `null` | 최종 수정한 사용자 정보 (uuid/name — updated_by 관계 파생, 없으면 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "추가배송비 템플릿 정보를 조회했습니다.", + "data": { + "id": 1, + "zipcode": "00000", + "fee": 3000, + "fee_formatted": "3,000원", + "region": null, + "description": null, + "is_active": true, + "created_by": null, + "updated_by": null, + "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 + } + } +} +``` **에러 응답** @@ -294,10 +544,32 @@ _요청 파라미터 없음._ | description | body | string | 아니오 | max 1000 | 설명 | | is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "zipcode": "06234", + "fee": 1, + "region": "예시값", + "description": "예시 내용입니다.", + "is_active": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -324,9 +596,64 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/1/toggle-active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| zipcode | string | `00000` | 우편번호 (단일 또는 범위) | +| fee | integer | `3000` | 추가 배송비 | +| fee_formatted | string | `3,000원` | `fee` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| region | null | `null` | 지역명 (예: 제주도, 울릉도) | +| description | null | `null` | 설명 (다국어 필드는 로케일별 값 객체) | +| is_active | boolean | `false` | active 여부 | +| created_by | null | `null` | 생성자 ID | +| updated_by | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 최종 수정한 사용자 정보 (uuid/name — updated_by 관계 파생, 없으면 null) | +| 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "추가배송비 템플릿 사용여부가 변경되었습니다.", + "data": { + "id": 1, + "zipcode": "00000", + "fee": 3000, + "fee_formatted": "3,000원", + "region": null, + "description": null, + "is_active": false, + "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 + } + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/guest.md b/modules/_bundled/sirsoft-ecommerce/docs/api/guest.md index d70a0949..545eb3d0 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/guest.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/guest.md @@ -31,10 +31,29 @@ | orderer_phone | body | string | 예 | max 20 | 주문자 전화번호 (본인 확인 키 ②) | | guest_lookup_password | body | string | 예 | max 255 | 비회원 주문 조회 비밀번호 (본인 확인 키 ③) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/guest/orders/verify HTTP/1.1 +Host: api.example.com +Accept: application/json +Content-Type: application/json + +{ + "order_number": "예시값", + "orderer_phone": "010-1234-5678", + "guest_lookup_password": "Password123!" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -62,10 +81,32 @@ | items | body | array | 아니오 | min 1 | 처리 대상 항목 배열 (전달 시 부분취소, 미전달 시 전체취소) | | refund_priority | body | string | 아니오 | `pg_first`, `points_first` | 환불 배분 우선순위 (PG 우선 / 포인트 우선, 미전달 시 pg_first) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/guest/orders/{orderNumber}/cancel HTTP/1.1 +Host: api.example.com +Accept: application/json +Content-Type: application/json + +{ + "reason": "예시값", + "reason_detail": "예시값", + "items": [ + "예시값" + ], + "refund_priority": "pg_first" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -92,10 +133,30 @@ | items | body | array | 예 | min 1 | 처리 대상 항목 배열 | | refund_priority | body | string | 아니오 | `pg_first`, `points_first` | 환불 배분 우선순위 (PG 우선 / 포인트 우선, 미전달 시 pg_first) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/guest/orders/{orderNumber}/estimate-refund HTTP/1.1 +Host: api.example.com +Accept: application/json +Content-Type: application/json + +{ + "items": [ + "예시값" + ], + "refund_priority": "pg_first" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -121,10 +182,22 @@ | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | | optionId | path | string | 예 | — | 대상 option의 식별자 | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/guest/orders/{orderNumber}/options/{optionId}/confirm HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -161,10 +234,39 @@ | intl_postal_code | body | string | 아니오 | max 20 | 우편번호 (국제 주소) | | delivery_memo | body | string | 아니오 | max 255 | 배송 메모 (배송 시 요청사항) | +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/guest/orders/{orderNumber}/shipping-address HTTP/1.1 +Host: api.example.com +Accept: application/json +Content-Type: application/json + +{ + "recipient_name": "예시 이름", + "recipient_phone": "010-1234-5678", + "recipient_tel": "예시값", + "country_code": "KR", + "zipcode": "06234", + "address": "서울특별시 강남구 테헤란로 1", + "address_detail": "서울특별시 강남구 테헤란로 1", + "address_line_1": "서울특별시 강남구 테헤란로 1", + "address_line_2": "서울특별시 강남구 테헤란로 1", + "intl_city": "예시값", + "intl_state": "예시값", + "intl_postal_code": "06234", + "delivery_memo": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/inquiries.md b/modules/_bundled/sirsoft-ecommerce/docs/api/inquiries.md index 4274c3ab..0e5d4a7f 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/inquiries.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/inquiries.md @@ -29,10 +29,23 @@ | --- | --- | --- | --- | --- | --- | | inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/inquiries/{inquiryId} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -58,10 +71,23 @@ | --- | --- | --- | --- | --- | --- | | inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/inquiries/{inquiryId}/reply HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -88,10 +114,28 @@ | inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | | content | body | string | 예 | min 1, max 5000 | 본문 내용 | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/inquiries/{inquiryId}/reply HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "content": "예시 내용입니다." +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -119,10 +163,28 @@ | inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | | content | body | string | 예 | min 1, max 5000 | 본문 내용 | +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/inquiries/{inquiryId}/reply HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "content": "예시 내용입니다." +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -147,6 +209,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/inquiries HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -156,6 +227,69 @@ _단건 응답: `data` 객체의 필드._ | items | array | `[]` | 내 문의 항목 배열 (각 항목: id, product 요약, product_name, is_answered, 게시판 연동 시 title/category/content/is_secret/reply/attachments) | | meta | object | `{"current_page":1,"per_page":25,"total":0,"last_page":1,"…` | 페이지네이션 메타 (current_page/per_page/total/last_page/from/to, 문의 게시판 연동 여부 inquiry_available, abilities 답변·삭제 권한, board_settings) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "문의 목록을 조회했습니다.", + "data": { + "items": [ + { + "id": 1, + "product_id": 1, + "product": { + "id": 1, + "name": "API 문서 샘플 상품", + "thumbnail_url": null, + "url": "/shop/products/1" + }, + "product_name": "iste et inventore", + "is_answered": false, + "answered_at": null, + "created_at": "2026-07-08T01:44:49+00:00", + "updated_at": "2026-07-08T01:44:49+00:00", + "title": null, + "category": null, + "content": null, + "is_secret": "{MASKED}", + "reply": null, + "attachments": [] + } + ], + "meta": { + "current_page": 1, + "per_page": 25, + "total": 1, + "last_page": 1, + "from": 1, + "to": 1, + "inquiry_available": false, + "abilities": { + "can_update": true, + "can_delete": true + }, + "board_settings": { + "secret_mode": "{MASKED}", + "categories": [], + "use_file_upload": false, + "max_file_count": 5, + "max_file_size": 10485760, + "allowed_extensions": [], + "min_title_length": 2, + "max_title_length": 200, + "min_content_length": 10, + "max_content_length": 10000 + } + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -179,10 +313,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/user/inquiries/{inquiryId} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -213,10 +360,31 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.inquiry.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/user/inquiries/{inquiryId} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "title": "예시 제목", + "category": "예시값", + "content": "예시 내용입니다.", + "is_secret": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -242,10 +410,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/user/inquiries/{inquiryId}/reply HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -271,10 +452,28 @@ _단건 응답: `data` 객체의 필드._ | inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | | content | body | string | 예 | min 1, max 5000 | 본문 내용 | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/user/inquiries/{inquiryId}/reply HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "content": "예시 내용입니다." +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -301,10 +500,28 @@ _단건 응답: `data` 객체의 필드._ | inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | | content | body | string | 예 | min 1, max 5000 | 본문 내용 | +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/user/inquiries/{inquiryId}/reply HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "content": "예시 내용입니다." +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/mileage-transactions.md b/modules/_bundled/sirsoft-ecommerce/docs/api/mileage-transactions.md index 1eeaea09..c177968a 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/mileage-transactions.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/mileage-transactions.md @@ -37,6 +37,15 @@ | start_date | query | date | 아니오 | — | 조회 기간 시작일 (이 날짜 이후 데이터) | | end_date | query | date | 아니오 | — | 조회 기간 종료일 (이 날짜 이전 데이터) | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/mileage-transactions?page=1&per_page=1&sort=created_at_desc&search_field=member&search_keyword=%EC%98%88%EC%8B%9C%EA%B0%92&type=earn¤cy=%EC%98%88%EC%8B%9C%EA%B0%92&start_date=2026-01-01&end_date=2026-01-01 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -83,6 +92,82 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | expiry_state | string | `active` | 적립건 소멸 상태 (active=미소멸, partial_expired=일부 소멸, fully_expired=전액 소멸 — 적립계만 의미) | | abilities | object | `{"can_manage":true,"can_edit":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "마일리지 내역을 조회했습니다.", + "data": { + "data": [ + { + "number": 1, + "id": 1, + "user_id": 6, + "currency": "KRW", + "type": "admin_earn", + "type_label": "관리자 지급", + "admin_badge_group": "amber", + "user_display_category": "adjust", + "amount": 1000, + "amount_formatted": "1,000원", + "remaining_amount": 0, + "remaining_amount_formatted": "0원", + "balance_after": 1000, + "order_id": null, + "order_option_id": null, + "order_cancel_id": null, + "source_transaction_id": null, + "granted_by": null, + "granted_by_name": null, + "granted_by_uuid": null, + "user_name": "API 문서 샘플 사용자", + "user_uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "order_number": null, + "description": null, + "memo": null, + "expires_at": null, + "expires_at_formatted": null, + "expires_at_date": null, + "expired_at": null, + "expired_at_formatted": null, + "created_at": "2026-07-08T01:44:49+00:00", + "created_at_formatted": "2026-07-08 10:44:49", + "created_at_date": "2026-07-08", + "is_earning": true, + "can_edit_expiry": false, + "expired_amount": 0, + "expired_amount_formatted": "0원", + "expiry_state": "active", + "abilities": { + "can_manage": true, + "can_edit": true + } + } + ], + "abilities": { + "can_manage": true + }, + "currencies": [ + "KRW" + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -115,10 +200,35 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | expires_at | body | date | 아니오 | — | 만료일 직접 지정 (지급 시 `use_default_expiry`=false 인 경우 적용) | | use_default_expiry | body | boolean | 아니오 | — | 지급 시 정책 기본 만료일 적용 여부 (기본 true, false 면 `expires_at` 사용) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/mileage-transactions HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "user_id": "9f8b2c1a-4d3e-4a2b-8c1d-0e1f2a3b4c5d", + "action": "earn", + "amount": 1, + "currency": "예시값", + "memo": "예시값", + "description": "예시 내용입니다.", + "expires_at": "2026-01-01", + "use_default_expiry": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -146,10 +256,32 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | lot_ids | body | array | 예 | min 1 | lot 식별자 배열 | | days | body | integer | 예 | min 1, max 3650 | 각 lot 만료일을 연장할 일수 (최대 3650일) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/mileage-transactions/extend-expiry HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "user_id": "9f8b2c1a-4d3e-4a2b-8c1d-0e1f2a3b4c5d", + "lot_ids": [ + "예시값" + ], + "days": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -177,10 +309,29 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | memo | body | string | 아니오 | max 1000 | 관리자 메모 보정값 (요청에 포함된 경우만 갱신, 빈 값으로 비우기 허용) | | expires_at | body | date | 아니오 | — | expires 일시 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/mileage-transactions/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "memo": "예시값", + "expires_at": "2026-01-01" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -207,9 +358,40 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/mileage-transactions/1/linked HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_목록 응답: `data.data[]` 배열 항목의 필드._ + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "연결 거래를 조회했습니다.", + "data": { + "data": [], + "abilities": { + "can_manage": true + }, + "currencies": [] + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/mileage.md b/modules/_bundled/sirsoft-ecommerce/docs/api/mileage.md index 366f28be..ec3f163a 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/mileage.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/mileage.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/mileage HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -35,6 +44,31 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | mileage | object | `{"enabled":false,"available":12910,"pending":14000,"expir…` | 마일리지 잔액 요약 객체 (enabled 기능 활성화 여부, available 사용 가능, pending 적립 대기, expiring_soon/expiring_date 소멸 예정, total_earned/total_used 누적 적립·사용, by_currency 통화별 잔액) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "마일리지 잔액을 조회했습니다.", + "data": { + "mileage": { + "enabled": false, + "available": 0, + "pending": 0, + "expiring_soon": 0, + "expiring_date": null, + "total_earned": 0, + "total_used": 0, + "by_currency": [] + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -60,6 +94,15 @@ _단건 응답: `data` 객체의 필드._ | currency | query | string | 아니오 | max 10 | 통화 코드 필터 (해당 통화의 마일리지 거래만 조회) | | per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/mileage/history?category=earn¤cy=%EC%98%88%EC%8B%9C%EA%B0%92&per_page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -68,6 +111,82 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | transactions | object | `{"data":[{"number":6,"id":527,"user_id":1,"currency":"KRW…` | 마일리지 거래 내역 페이지네이션 객체 (`data` 거래 항목 배열 + 페이지 메타, `MileageTransactionCollection` 으로 직렬화) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "마일리지 내역을 조회했습니다.", + "data": { + "transactions": { + "data": [ + { + "number": 1, + "id": 1, + "user_id": 6, + "currency": "KRW", + "type": "admin_earn", + "type_label": "관리자 지급", + "admin_badge_group": "amber", + "user_display_category": "adjust", + "amount": 1000, + "amount_formatted": "1,000원", + "remaining_amount": 0, + "remaining_amount_formatted": "0원", + "balance_after": 1000, + "order_id": null, + "order_option_id": null, + "order_cancel_id": null, + "source_transaction_id": null, + "granted_by": null, + "granted_by_name": [], + "granted_by_uuid": [], + "user_name": [], + "user_uuid": [], + "order_number": [], + "description": null, + "memo": null, + "expires_at": null, + "expires_at_formatted": null, + "expires_at_date": null, + "expired_at": null, + "expired_at_formatted": null, + "created_at": "2026-07-08T01:44:49+00:00", + "created_at_formatted": "2026-07-08 10:44:49", + "created_at_date": "2026-07-08", + "is_earning": true, + "can_edit_expiry": false, + "expired_amount": 0, + "expired_amount_formatted": "0원", + "expiry_state": "active", + "abilities": { + "can_manage": true, + "can_edit": true + } + } + ], + "abilities": { + "can_manage": true + }, + "currencies": [], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + } + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -94,10 +213,23 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.mileage.max_usable_validation_rules`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/mileage/max-usable?order_amount=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/options.md b/modules/_bundled/sirsoft-ecommerce/docs/api/options.md index 5292df17..a9748ed7 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/options.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/options.md @@ -35,10 +35,36 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product_option.bulk_price_validation_rules`). +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/options/bulk-price HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "product_ids": [ + "예시값" + ], + "option_ids": [ + "예시값" + ], + "method": "increase", + "value": 1, + "unit": "won" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -69,10 +95,35 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product_option.bulk_stock_validation_rules`). +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/options/bulk-stock HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "product_ids": [ + "예시값" + ], + "option_ids": [ + "예시값" + ], + "method": "increase", + "value": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -102,10 +153,36 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.option.bulk_update_validation_rules`). +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/options/bulk-update HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "bulk_changes": [ + "예시값" + ], + "items": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/orders.md b/modules/_bundled/sirsoft-ecommerce/docs/api/orders.md index fb796245..bb6a447b 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/orders.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/orders.md @@ -54,6 +54,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.order.list_validation_rules`, `sirsoft-ecommerce.order.list_validation_messages`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/orders?search_field=all&search_keyword=%EC%98%88%EC%8B%9C%EA%B0%92&date_type=%EC%98%88%EC%8B%9C%EA%B0%92&start_date=2026-01-01&end_date=2026-01-01&order_status=%EC%98%88%EC%8B%9C%EA%B0%92&option_status=%EC%98%88%EC%8B%9C%EA%B0%92&shipping_type=%EC%98%88%EC%8B%9C%EA%B0%92&payment_method=%EC%98%88%EC%8B%9C%EA%B0%92&category_id=1&min_amount=1&max_amount=1&country_codes=KR&order_device=%EC%98%88%EC%8B%9C%EA%B0%92&min_shipping_amount=1&max_shipping_amount=1&shipping_policy_id=1&user_id=1&orderer_uuid=9f8b2c1a-4d3e-4a2b-8c1d-0e1f2a3b4c5d&member_type=member&sort_by=ordered_at&sort_order=asc&per_page=1&page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -98,6 +107,160 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_read":true,"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "주문 정보를 조회했습니다.", + "data": { + "data": [ + { + "number": 2, + "id": 5, + "order_number": "ORD-20260708-000002", + "order_status": "pending_payment", + "order_status_label": "결제대기", + "order_status_variant": "warning", + "base_currency": "KRW", + "payment_currency": "KRW", + "is_cross_currency": false, + "is_partially_cancelled": false, + "total_amount": 163969, + "total_amount_formatted": "163,969원", + "total_shipping_amount": 2500, + "total_shipping_amount_formatted": "2,500원", + "total_paid_amount": 0, + "total_paid_amount_formatted": "0원", + "total_unpaid_amount": 163969, + "total_unpaid_amount_formatted": "163,969원", + "total_cancelled_amount": 0, + "total_refunded_amount": 0, + "total_points_used_amount": 0, + "total_points_used_amount_formatted": "0원", + "total_earned_points_amount": 1640, + "total_earned_points_amount_formatted": "1,640원", + "ordered_at": "2026-07-08T01:44:49+00:00", + "ordered_at_formatted": "2026-07-08 10:44:49", + "order_device": "pc", + "order_device_label": "PC", + "is_first_order": true, + "user": { + "uuid": "a234c3ea-a4f9-496d-a5e7-b44f0d53fc2f", + "name": "선지영" + }, + "first_option": { + "product_name": "assumenda doloremque ipsam", + "product_option_name": "ea", + "product_code": null, + "quantity": 2, + "thumbnail_url": null, + "additional_options_summary": null + }, + "options_count": 1, + "address": null, + "payment": null, + "shipping": { + "shipping_type": null, + "shipping_type_label": null, + "shipping_method_label": null, + "carrier_name": null, + "tracking_number": null + }, + "is_owner": false, + "abilities": { + "can_read": true, + "can_update": true + } + }, + { + "number": 1, + "id": 4, + "order_number": "APIDOC-20260708-000001", + "order_status": "pending_payment", + "order_status_label": "결제대기", + "order_status_variant": "warning", + "base_currency": "KRW", + "payment_currency": "KRW", + "is_cross_currency": false, + "is_partially_cancelled": false, + "total_amount": 327327, + "total_amount_formatted": "327,327원", + "total_shipping_amount": 3000, + "total_shipping_amount_formatted": "3,000원", + "total_paid_amount": 0, + "total_paid_amount_formatted": "0원", + "total_unpaid_amount": 327327, + "total_unpaid_amount_formatted": "327,327원", + "total_cancelled_amount": 0, + "total_refunded_amount": 0, + "total_points_used_amount": 0, + "total_points_used_amount_formatted": "0원", + "total_earned_points_amount": 3273, + "total_earned_points_amount_formatted": "3,273원", + "ordered_at": "2026-07-08T01:44:49+00:00", + "ordered_at_formatted": "2026-07-08 10:44:49", + "order_device": "pc", + "order_device_label": "PC", + "is_first_order": false, + "user": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자" + }, + "first_option": { + "product_name": "", + "product_option_name": "", + "product_code": null, + "quantity": null, + "thumbnail_url": null, + "additional_options_summary": null + }, + "options_count": 0, + "address": null, + "payment": null, + "shipping": { + "shipping_type": null, + "shipping_type_label": null, + "shipping_method_label": null, + "carrier_name": null, + "tracking_number": null + }, + "is_owner": true, + "abilities": { + "can_read": true, + "can_update": true + } + } + ], + "abilities": { + "can_update": true + }, + "statistics": { + "total": 2, + "status_counts": { + "pending_payment": 2 + }, + "today_count": 2, + "today_revenue": "0.00", + "monthly_revenue": "0.00" + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 2, + "from": 1, + "to": 2, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -126,10 +289,33 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | carrier_id | body | integer | 아니오 | — | carrier 식별자 | | tracking_number | body | string | 아니오 | max 50 | 송장(운송장)번호 (배송 관련 상태로 전환 시 carrier_id 와 함께 필수) | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/orders/bulk HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "order_status": "예시값", + "carrier_id": 1, + "tracking_number": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -155,10 +341,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | order | path | string | 예 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/orders/4 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -184,6 +383,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | order | path | string | 예 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/orders/4 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -297,6 +505,135 @@ _단건 응답: `data` 객체의 필드._ | is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_read":true,"can_update":true,"can_cancel":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "주문 정보를 조회했습니다.", + "data": { + "id": 4, + "order_number": "APIDOC-20260708-000001", + "base_currency": "KRW", + "payment_currency": "KRW", + "is_cross_currency": false, + "order_status": "pending_payment", + "order_status_label": "결제대기", + "order_status_variant": "warning", + "is_partially_cancelled": false, + "order_device": "pc", + "order_device_label": "PC", + "is_first_order": false, + "subtotal_amount": 324327, + "subtotal_amount_formatted": "324,327원", + "total_discount_amount": 0, + "total_discount_amount_formatted": "0원", + "total_shipping_amount": 3000, + "total_shipping_amount_formatted": "3,000원", + "total_amount": 327327, + "total_amount_formatted": "327,327원", + "total_paid_amount": 0, + "total_paid_amount_formatted": "0원", + "total_due_amount": 327327, + "total_due_amount_formatted": "327,327원", + "depositor_name": null, + "total_cancelled_amount": 0, + "total_cancelled_amount_formatted": "0원", + "total_refunded_amount": 0, + "total_refunded_amount_formatted": "0원", + "total_refunded_points_amount": 0, + "total_refunded_points_amount_formatted": "0원", + "total_product_coupon_discount_amount": 0, + "total_product_coupon_discount_amount_formatted": "0원", + "total_order_coupon_discount_amount": 0, + "total_order_coupon_discount_amount_formatted": "0원", + "total_coupon_discount_amount": 0, + "total_coupon_discount_amount_formatted": "0원", + "total_code_discount_amount": 0, + "total_code_discount_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": 3273, + "total_earned_points_amount_formatted": "3,273원", + "mc_subtotal_amount": [], + "mc_total_discount_amount": [], + "mc_total_shipping_amount": [], + "mc_total_amount": [], + "mc_total_product_coupon_discount_amount": [], + "mc_total_order_coupon_discount_amount": [], + "mc_total_coupon_discount_amount": [], + "mc_total_code_discount_amount": [], + "mc_total_points_used_amount": [], + "mc_total_deposit_used_amount": [], + "item_count": 4, + "total_quantity": 0, + "total_list_price": 0, + "total_list_price_formatted": "0원", + "ordered_at": "2026-07-08T01:44:49+00:00", + "ordered_at_formatted": "2026-07-08 10:44:49", + "paid_at": null, + "paid_at_formatted": null, + "confirmed_at": null, + "confirmed_at_formatted": null, + "cancelled_at": null, + "cancelled_at_formatted": null, + "delivered_at": null, + "total_tax_amount": 29757, + "total_tax_amount_formatted": "29,757원", + "total_vat_amount": 0, + "total_vat_amount_formatted": "0원", + "total_taxable_supply_amount": 29757, + "total_taxable_supply_amount_formatted": "29,757원", + "total_tax_free_amount": 0, + "total_tax_free_amount_formatted": "0원", + "user": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" + }, + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "user_login_id": null, + "orderer_name": null, + "orderer_phone": null, + "orderer_tel": null, + "orderer_email": null, + "recipient_name": null, + "recipient_phone": null, + "recipient_tel": null, + "recipient_zipcode": null, + "recipient_address": null, + "recipient_detail_address": null, + "delivery_memo": null, + "delivery_memo_label": null, + "options": [], + "shipping_address": null, + "billing_address": null, + "payment": null, + "payments": [], + "shippings": [], + "cancels": [], + "promotions_applied_snapshot": null, + "shipping_policy_applied_snapshot": null, + "admin_memo": null, + "customer_memo": null, + "created_at": "2026-07-08T01:44:49+00:00", + "updated_at": "2026-07-08T01:44:49+00:00", + "is_owner": true, + "abilities": { + "can_read": true, + "can_update": true, + "can_cancel": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -337,10 +674,42 @@ _단건 응답: `data` 객체의 필드._ | delivery_memo | body | string | 아니오 | max 500 | 배송 메모 (배송 시 요청사항) | | recipient_country_code | body | string | 아니오 | — | 수령인 배송국가 코드 (ISO 3166-1 alpha-2 2자리) | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/orders/4 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "order_status": "예시값", + "admin_memo": "예시값", + "recipient_name": "예시 이름", + "recipient_phone": "010-1234-5678", + "recipient_tel": "예시값", + "recipient_zipcode": "06234", + "recipient_address": "서울특별시 강남구 테헤란로 1", + "recipient_detail_address": "서울특별시 강남구 테헤란로 1", + "address_line_1": "서울특별시 강남구 테헤란로 1", + "address_line_2": "서울특별시 강남구 테헤란로 1", + "intl_city": "예시값", + "intl_state": "예시값", + "intl_postal_code": "06234", + "delivery_memo": "예시값", + "recipient_country_code": "KR" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -373,10 +742,35 @@ _단건 응답: `data` 객체의 필드._ | cancel_pg | body | boolean | 아니오 | — | PG 결제 취소 동반 여부 (미지정 시 기본 true — 실제 PG 취소 수행) | | refund_priority | body | string | 아니오 | `pg_first`, `points_first` | 환불 배분 우선순위 (pg_first PG 우선 / points_first 포인트 우선, 기본 pg_first) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/orders/4/cancel HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "type": "예시값", + "reason": "예시값", + "reason_detail": "예시값", + "items": [ + "예시값" + ], + "cancel_pg": true, + "refund_priority": "pg_first" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -406,10 +800,30 @@ _단건 응답: `data` 객체의 필드._ | depositor_name | body | string | 아니오 | max 100 | depositor 이름 (식별자) | | mark_order_complete | body | boolean | 아니오 | — | 입금확인과 동시에 주문완료 처리 여부 (미지정 시 기본 false — 결제완료 전이만) | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/orders/4/confirm-deposit HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "amount": 1, + "depositor_name": "예시 이름", + "mark_order_complete": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -438,10 +852,31 @@ _단건 응답: `data` 객체의 필드._ | items | body | array | 예 | min 1 | 처리 대상 항목 배열 | | refund_priority | body | string | 아니오 | `pg_first`, `points_first` | 환불 배분 우선순위 (pg_first PG 우선 / points_first 포인트 우선, 기본 pg_first) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/orders/4/estimate-refund HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "items": [ + "예시값" + ], + "refund_priority": "pg_first" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -470,12 +905,72 @@ _단건 응답: `data` 객체의 필드._ | per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | | sort_order | query | string | 아니오 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/orders/4/logs?per_page=1&sort_order=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "주문 처리 이력을 조회했습니다.", + "data": { + "data": [], + "links": { + "first": "https://api.example.com/api/modules/sirsoft-ecommerce/admin/orders/4/logs?page=1", + "last": "https://api.example.com/api/modules/sirsoft-ecommerce/admin/orders/4/logs?page=1", + "prev": null, + "next": null + }, + "meta": { + "current_page": 1, + "from": null, + "last_page": 1, + "links": [ + { + "url": null, + "label": "pagination.previous", + "page": null, + "active": false + }, + { + "url": "https://api.example.com/api/modules/sirsoft-ecommerce/admin/orders/4/logs?page=1", + "label": "1", + "page": 1, + "active": true + }, + { + "url": null, + "label": "pagination.next", + "page": null, + "active": false + } + ], + "path": "https://api.example.com/api/modules/sirsoft-ecommerce/admin/orders/4/logs", + "per_page": 25, + "to": null, + "total": 0 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -506,10 +1001,33 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | carrier_id | body | integer | 아니오 | — | carrier 식별자 | | tracking_number | body | string | 아니오 | max 50 | 송장(운송장)번호 (배송 관련 상태로 전환 시 carrier_id 와 함께 필수) | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/orders/4/options/bulk-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "items": [ + "예시값" + ], + "status": "예시값", + "carrier_id": 1, + "tracking_number": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -538,10 +1056,29 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | guest_lookup_password | body | string | 예 | min 8, max 255 | 재설정할 비회원 주문 조회 비밀번호 (8자 이상, 해시로 저장 · 회원가입 정책과 동일) | | guest_lookup_password_confirmation | body | string | 예 | — | 조회 비밀번호 확인 (guest_lookup_password 와 일치해야 함) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/orders/4/reset-guest-lookup-password HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "guest_lookup_password": "Password123!", + "guest_lookup_password_confirmation": "Password123!" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -570,10 +1107,29 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | email | body | email | 예 | max 255 | 이메일 주소 | | message | body | string | 예 | max 5000 | 관리자가 작성한 안내 메일 본문 | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/orders/4/send-email HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "email": "user@example.com", + "message": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -602,10 +1158,29 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | cancel_code | body | string | 아니오 | max 100 | PG사 취소 코드 (예: USER_CANCEL, order_payments 취소 이력에 기록) | | cancel_message | body | string | 아니오 | max 500 | PG사 취소 메시지 (order_payments 취소 이력에 기록) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/orders/APIDOC-20260708-000001/cancel-payment HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +Content-Type: application/json + +{ + "cancel_code": "예시값", + "cancel_message": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -632,6 +1207,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | per_page | query | integer | 아니오 | min 1, max 50 | 페이지당 항목 수 | | status | query | string | 아니오 | — | 상태 필터 (해당 상태의 항목만 조회) | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/orders?page=1&per_page=1&status=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -662,6 +1246,68 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | item_count | integer | `1` | item 개수 (집계) | | abilities | object | `{"can_view":true,"can_cancel":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "주문 정보를 조회했습니다.", + "data": { + "data": [ + { + "id": 4, + "order_number": "APIDOC-20260708-000001", + "status": "pending_payment", + "status_label": "결제대기", + "status_variant": "warning", + "is_partially_cancelled": false, + "recipient_country_code": null, + "recipient_country_name": null, + "ordered_at": "2026-07-08T01:44:49+00:00", + "ordered_at_formatted": "2026-07-08 10:44:49", + "total_amount": 327327, + "total_amount_formatted": "327,327원", + "mc_total_amount": [], + "total_shipping_amount": 3000, + "total_shipping_amount_formatted": "3,000원", + "mc_total_shipping_amount": [], + "total_points_used_amount": 0, + "total_points_used_amount_formatted": "0원", + "total_earned_points_amount": 3273, + "total_earned_points_amount_formatted": "3,273원", + "items": [], + "item_count": 0, + "abilities": { + "can_view": true, + "can_cancel": true + } + } + ], + "statistics": { + "pending_payment": 1, + "payment_complete": 0, + "preparing": 0, + "shipping": 0, + "delivered": 0, + "confirmed": 0 + }, + "abilities": { + "can_create": true + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 1 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -694,10 +1340,34 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.order.create_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/user/orders HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +Content-Type: application/json + +{ + "payment_method": "예시값", + "expected_total_amount": 1, + "shipping_memo": "예시값", + "depositor_name": "예시 이름", + "save_shipping_address": true, + "guest_lookup_password": "Password123!", + "guest_lookup_password_confirmation": "Password123!" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -722,9 +1392,256 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/orders/4 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `4` | 기본 키 (내부 식별자) | +| order_number | string | `APIDOC-20260708-000001` | 주문번호 | +| base_currency | string | `KRW` | 금액 표기 기준 통화 (모든 *_formatted 필드의 통화, 주문 시점 base_currency 고정) | +| payment_currency | string | `KRW` | 결제 통화 (유저가 선택·결제한 통화, base_currency 와 다르면 병기 표시) | +| is_cross_currency | boolean | `false` | cross currency 여부 | +| order_status | string | `pending_payment` | 주문상태 (OrderStatusEnum) | +| order_status_label | string | `결제대기` | `order_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| order_status_variant | string | `warning` | `order_status` 값의 표시 변형 키 (UI 배지 색상/스타일) | +| is_partially_cancelled | boolean | `false` | partially cancelled 여부 | +| order_device | string | `pc` | 주문 디바이스 (pc/mobile/app) | +| order_device_label | string | `PC` | `order_device` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| is_first_order | boolean | `false` | first order 여부 | +| subtotal_amount | integer | `324327` | 상품 합계 (할인 전, 상품가×수량 합계) | +| subtotal_amount_formatted | string | `324,327원` | `subtotal_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_discount_amount | integer | `0` | 총 할인금액 (모든 할인 합계) | +| total_discount_amount_formatted | string | `0원` | `total_discount_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_shipping_amount | integer | `3000` | 총 배송비 | +| total_shipping_amount_formatted | string | `3,000원` | `total_shipping_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_amount | integer | `327327` | 최종 주문금액 (subtotal - discount + shipping) | +| total_amount_formatted | string | `327,327원` | `total_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_paid_amount | integer | `0` | 총 실제 결제금액 (PG 결제액) | +| total_paid_amount_formatted | string | `0원` | `total_paid_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_due_amount | integer | `327327` | 총 결제예정금액 (무통장 등) | +| total_due_amount_formatted | string | `327,327원` | `total_due_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| depositor_name | null | `null` | 무통장 입금자명 (입금확인 모달 기본값, payment 관계 로드 시에만 노출) | +| total_cancelled_amount | integer | `0` | 총 취소금액 | +| total_cancelled_amount_formatted | string | `0원` | `total_cancelled_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_refunded_amount | integer | `0` | 총 환불금액 | +| total_refunded_amount_formatted | string | `0원` | `total_refunded_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_refunded_points_amount | integer | `0` | 총 환불 포인트 | +| total_refunded_points_amount_formatted | string | `0원` | `total_refunded_points_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_product_coupon_discount_amount | integer | `0` | 상품 쿠폰 할인 합계 | +| total_product_coupon_discount_amount_formatted | string | `0원` | `total_product_coupon_discount_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_order_coupon_discount_amount | integer | `0` | 주문 쿠폰 할인 합계 | +| total_order_coupon_discount_amount_formatted | string | `0원` | `total_order_coupon_discount_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_coupon_discount_amount | integer | `0` | 총 쿠폰 할인금액 | +| total_coupon_discount_amount_formatted | string | `0원` | `total_coupon_discount_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_code_discount_amount | integer | `0` | 총 할인코드 할인금액 | +| total_code_discount_amount_formatted | string | `0원` | `total_code_discount_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_points_used_amount | integer | `0` | 총 포인트 사용액 | +| total_points_used_amount_formatted | string | `0원` | `total_points_used_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_deposit_used_amount | integer | `0` | 총 예치금 사용액 | +| total_deposit_used_amount_formatted | string | `0원` | `total_deposit_used_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_earned_points_amount | integer | `3273` | 총 적립 예정 포인트 | +| total_earned_points_amount_formatted | string | `3,273원` | `total_earned_points_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| mc_subtotal_amount | array | `[]` | 상품합계 다중 통화 | +| mc_total_discount_amount | array | `[]` | 총 할인 다중 통화 | +| mc_total_shipping_amount | array | `[]` | 총 배송비 다중 통화 | +| mc_total_amount | array | `[]` | 최종금액 다중 통화 (payment_amount) | +| mc_total_product_coupon_discount_amount | array | `[]` | 상품 쿠폰 할인 다중 통화 | +| mc_total_order_coupon_discount_amount | array | `[]` | 주문 쿠폰 할인 다중 통화 | +| mc_total_coupon_discount_amount | array | `[]` | 쿠폰 할인 합계 다중 통화 | +| mc_total_code_discount_amount | array | `[]` | 할인코드 할인 다중 통화 | +| mc_total_points_used_amount | array | `[]` | 포인트 사용 다중 통화 | +| mc_total_deposit_used_amount | array | `[]` | 예치금 사용 다중 통화 | +| item_count | integer | `4` | item 개수 (집계) | +| total_quantity | integer | `0` | 주문 옵션 수량 합계 (options 로드 시) | +| total_list_price | integer | `0` | 정가 합계 (옵션 스냅샷 정가 × 수량 합계) | +| total_list_price_formatted | string | `0원` | `total_list_price` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| ordered_at | string | `2026-07-08T01:44:49+00:00` | ordered 일시 | +| ordered_at_formatted | string | `2026-07-08 10:44:49` | `ordered_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| paid_at | null | `null` | paid 일시 | +| paid_at_formatted | null | `null` | `paid_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| confirmed_at | null | `null` | confirmed 일시 | +| confirmed_at_formatted | null | `null` | `confirmed_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| cancelled_at | null | `null` | cancelled 일시 | +| cancelled_at_formatted | null | `null` | `cancelled_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| delivered_at | null | `null` | delivered 일시 | +| total_tax_amount | integer | `29757` | 총 과세금액 | +| total_tax_amount_formatted | string | `29,757원` | `total_tax_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_vat_amount | integer | `0` | 총 부가세금액 | +| total_vat_amount_formatted | string | `0원` | `total_vat_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_taxable_supply_amount | integer | `29757` | 과세 공급가액 (총 과세금액 − 부가세, 영수증 과세금액 표시 SSoT) | +| total_taxable_supply_amount_formatted | string | `29,757원` | `total_taxable_supply_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_tax_free_amount | integer | `0` | 총 면세금액 | +| total_tax_free_amount_formatted | string | `0원` | `total_tax_free_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| user | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 대상 사용자 정보 객체 (uuid/name/email 등 — user 관계 파생) | +| user_id | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | user 식별자 (연관 리소스 참조) | +| user_login_id | null | `null` | 회원 로그인 아이디 (login_id, 비회원 주문이면 null) | +| orderer_name | null | `null` | 주문자 이름 (배송지에서 플래튼) | +| orderer_phone | null | `null` | 주문자 휴대전화 (배송지에서 플래튼) | +| orderer_tel | null | `null` | 주문자 일반전화 (배송지에서 플래튼, 미입력 시 null) | +| orderer_email | null | `null` | 주문자 이메일 (배송지에서 플래튼, 비회원 알림 수신 통로) | +| recipient_name | null | `null` | 수령인 이름 (배송지에서 플래튼) | +| recipient_phone | null | `null` | 수령인 휴대전화 (배송지에서 플래튼) | +| recipient_tel | null | `null` | 수령인 일반전화 (배송지에서 플래튼, 미입력 시 null) | +| recipient_zipcode | null | `null` | 수령인 우편번호 (배송지에서 플래튼) | +| recipient_address | null | `null` | 수령인 기본 주소 (배송지에서 플래튼) | +| recipient_detail_address | null | `null` | 수령인 상세 주소 (배송지에서 플래튼, 미입력 시 null) | +| delivery_memo | null | `null` | 배송 메모 (배송지에서 플래튼, 미입력 시 null) | +| delivery_memo_label | null | `null` | `delivery_memo` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| options | array | `[]` | 주문 옵션(품목) 목록 (OrderOptionResource — 상품·옵션·수량·옵션상태·금액) | +| shipping_address | null | `null` | 배송지 상세 (OrderAddressResource — 주문자/수령인/국내·해외 주소) | +| billing_address | null | `null` | 청구지 상세 (OrderAddressResource, 미분리 시 null) | +| payment | null | `null` | 대표 결제 정보 (OrderPaymentResource — 결제수단·결제상태·금액) | +| payments | array | `[]` | 결제 이력 목록 (OrderPaymentResource 배열 — 다회 결제/부분결제 포함) | +| shippings | array | `[]` | 배송 이력 목록 (OrderShippingResource 배열 — 배송유형·택배사·송장번호) | +| cancels | array | `[]` | 취소 이력 목록 (OrderCancelResource 배열 — 취소 사유·상세·취소일시, 최근순) | +| promotions_applied_snapshot | null | `null` | 적용된 프로모션 스냅샷 (재계산용) | +| shipping_policy_applied_snapshot | null | `null` | 적용된 배송정책 스냅샷 (재계산용) | +| admin_memo | null | `null` | 관리자 메모 (내부 관리용) | +| customer_memo | null | `null` | 고객 메모 (주문 시 고객이 남긴 메모) | +| created_at | string | `2026-07-08T01:44:49+00:00` | 생성 일시 | +| updated_at | string | `2026-07-08T01:44:49+00:00` | 최종 수정 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_update":true,"can_cancel":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "주문 정보를 조회했습니다.", + "data": { + "id": 4, + "order_number": "APIDOC-20260708-000001", + "base_currency": "KRW", + "payment_currency": "KRW", + "is_cross_currency": false, + "order_status": "pending_payment", + "order_status_label": "결제대기", + "order_status_variant": "warning", + "is_partially_cancelled": false, + "order_device": "pc", + "order_device_label": "PC", + "is_first_order": false, + "subtotal_amount": 324327, + "subtotal_amount_formatted": "324,327원", + "total_discount_amount": 0, + "total_discount_amount_formatted": "0원", + "total_shipping_amount": 3000, + "total_shipping_amount_formatted": "3,000원", + "total_amount": 327327, + "total_amount_formatted": "327,327원", + "total_paid_amount": 0, + "total_paid_amount_formatted": "0원", + "total_due_amount": 327327, + "total_due_amount_formatted": "327,327원", + "depositor_name": null, + "total_cancelled_amount": 0, + "total_cancelled_amount_formatted": "0원", + "total_refunded_amount": 0, + "total_refunded_amount_formatted": "0원", + "total_refunded_points_amount": 0, + "total_refunded_points_amount_formatted": "0원", + "total_product_coupon_discount_amount": 0, + "total_product_coupon_discount_amount_formatted": "0원", + "total_order_coupon_discount_amount": 0, + "total_order_coupon_discount_amount_formatted": "0원", + "total_coupon_discount_amount": 0, + "total_coupon_discount_amount_formatted": "0원", + "total_code_discount_amount": 0, + "total_code_discount_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": 3273, + "total_earned_points_amount_formatted": "3,273원", + "mc_subtotal_amount": [], + "mc_total_discount_amount": [], + "mc_total_shipping_amount": [], + "mc_total_amount": [], + "mc_total_product_coupon_discount_amount": [], + "mc_total_order_coupon_discount_amount": [], + "mc_total_coupon_discount_amount": [], + "mc_total_code_discount_amount": [], + "mc_total_points_used_amount": [], + "mc_total_deposit_used_amount": [], + "item_count": 4, + "total_quantity": 0, + "total_list_price": 0, + "total_list_price_formatted": "0원", + "ordered_at": "2026-07-08T01:44:49+00:00", + "ordered_at_formatted": "2026-07-08 10:44:49", + "paid_at": null, + "paid_at_formatted": null, + "confirmed_at": null, + "confirmed_at_formatted": null, + "cancelled_at": null, + "cancelled_at_formatted": null, + "delivered_at": null, + "total_tax_amount": 29757, + "total_tax_amount_formatted": "29,757원", + "total_vat_amount": 0, + "total_vat_amount_formatted": "0원", + "total_taxable_supply_amount": 29757, + "total_taxable_supply_amount_formatted": "29,757원", + "total_tax_free_amount": 0, + "total_tax_free_amount_formatted": "0원", + "user": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" + }, + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "user_login_id": null, + "orderer_name": null, + "orderer_phone": null, + "orderer_tel": null, + "orderer_email": null, + "recipient_name": null, + "recipient_phone": null, + "recipient_tel": null, + "recipient_zipcode": null, + "recipient_address": null, + "recipient_detail_address": null, + "delivery_memo": null, + "delivery_memo_label": null, + "options": [], + "shipping_address": null, + "billing_address": null, + "payment": null, + "payments": [], + "shippings": [], + "cancels": [], + "promotions_applied_snapshot": null, + "shipping_policy_applied_snapshot": null, + "admin_memo": null, + "customer_memo": null, + "created_at": "2026-07-08T01:44:49+00:00", + "updated_at": "2026-07-08T01:44:49+00:00", + "is_owner": true, + "abilities": { + "can_read": true, + "can_update": true, + "can_cancel": true + } + } +} +``` **에러 응답** @@ -754,10 +1671,33 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | items | body | array | 아니오 | min 1 | 처리 대상 항목 배열 | | refund_priority | body | string | 아니오 | `pg_first`, `points_first` | 환불 배분 우선순위 (pg_first PG 우선 / points_first 포인트 우선, 기본 pg_first) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/user/orders/4/cancel HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "reason": "예시값", + "reason_detail": "예시값", + "items": [ + "예시값" + ], + "refund_priority": "pg_first" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -786,10 +1726,31 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | items | body | array | 예 | min 1 | 처리 대상 항목 배열 | | refund_priority | body | string | 아니오 | `pg_first`, `points_first` | 환불 배분 우선순위 (pg_first PG 우선 / points_first 포인트 우선, 기본 pg_first) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/user/orders/4/estimate-refund HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "items": [ + "예시값" + ], + "refund_priority": "pg_first" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -817,10 +1778,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | id | path | string | 예 | — | 대상 리소스의 식별자 | | optionId | path | string | 예 | — | 대상 option의 식별자 | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/user/orders/4/options/{optionId}/confirm HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -846,9 +1820,42 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/user/orders/4/reorder HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| added_count | integer | `0` | added 개수 (집계) | +| skipped | array | `[]` | 장바구니에 담지 못한 항목 목록 (품절·단종 등으로 재주문에서 건너뛴 옵션) | +| cart_count | integer | `0` | cart 개수 (집계) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "과거 주문의 상품을 장바구니에 추가했습니다.", + "data": { + "added_count": 0, + "skipped": [], + "cart_count": 0 + } +} +``` **에러 응답** @@ -889,9 +1896,159 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.order.shipping_address_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/user/orders/4/shipping-address HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "address_id": 1, + "recipient_name": "예시 이름", + "recipient_phone": "010-1234-5678", + "country_code": "KR", + "zipcode": "06234", + "address": "서울특별시 강남구 테헤란로 1", + "address_detail": "서울특별시 강남구 테헤란로 1", + "address_line_1": "서울특별시 강남구 테헤란로 1", + "address_line_2": "서울특별시 강남구 테헤란로 1", + "intl_city": "예시값", + "intl_state": "예시값", + "intl_postal_code": "06234", + "delivery_memo": "예시값" +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| order | object | `{"id":4,"order_number":"APIDOC-20260708-000001","base_cur…` | 배송지 변경이 반영된 주문 상세 (OrderResource — 변경 후 최신 주문 전체) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송지가 변경되었습니다.", + "data": { + "order": { + "id": 4, + "order_number": "APIDOC-20260708-000001", + "base_currency": "KRW", + "payment_currency": "KRW", + "is_cross_currency": false, + "order_status": "pending_payment", + "order_status_label": "결제대기", + "order_status_variant": "warning", + "is_partially_cancelled": false, + "order_device": "pc", + "order_device_label": "PC", + "is_first_order": false, + "subtotal_amount": 324327, + "subtotal_amount_formatted": "324,327원", + "total_discount_amount": 0, + "total_discount_amount_formatted": "0원", + "total_shipping_amount": 3000, + "total_shipping_amount_formatted": "3,000원", + "total_amount": 327327, + "total_amount_formatted": "327,327원", + "total_paid_amount": 0, + "total_paid_amount_formatted": "0원", + "total_due_amount": 327327, + "total_due_amount_formatted": "327,327원", + "total_cancelled_amount": 0, + "total_cancelled_amount_formatted": "0원", + "total_refunded_amount": 0, + "total_refunded_amount_formatted": "0원", + "total_refunded_points_amount": 0, + "total_refunded_points_amount_formatted": "0원", + "total_product_coupon_discount_amount": 0, + "total_product_coupon_discount_amount_formatted": "0원", + "total_order_coupon_discount_amount": 0, + "total_order_coupon_discount_amount_formatted": "0원", + "total_coupon_discount_amount": 0, + "total_coupon_discount_amount_formatted": "0원", + "total_code_discount_amount": 0, + "total_code_discount_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": 3273, + "total_earned_points_amount_formatted": "3,273원", + "mc_subtotal_amount": [], + "mc_total_discount_amount": [], + "mc_total_shipping_amount": [], + "mc_total_amount": [], + "mc_total_product_coupon_discount_amount": [], + "mc_total_order_coupon_discount_amount": [], + "mc_total_coupon_discount_amount": [], + "mc_total_code_discount_amount": [], + "mc_total_points_used_amount": [], + "mc_total_deposit_used_amount": [], + "item_count": 4, + "total_quantity": 0, + "total_list_price": 0, + "total_list_price_formatted": "0원", + "ordered_at": "2026-07-08T01:44:49+00:00", + "ordered_at_formatted": "2026-07-08 10:44:49", + "paid_at": null, + "paid_at_formatted": null, + "confirmed_at": null, + "confirmed_at_formatted": null, + "cancelled_at": null, + "cancelled_at_formatted": null, + "delivered_at": null, + "total_tax_amount": 29757, + "total_tax_amount_formatted": "29,757원", + "total_vat_amount": 0, + "total_vat_amount_formatted": "0원", + "total_taxable_supply_amount": 29757, + "total_taxable_supply_amount_formatted": "29,757원", + "total_tax_free_amount": 0, + "total_tax_free_amount_formatted": "0원", + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "user_login_id": null, + "orderer_name": null, + "orderer_phone": null, + "orderer_tel": null, + "orderer_email": null, + "recipient_name": null, + "recipient_phone": null, + "recipient_tel": null, + "recipient_zipcode": null, + "recipient_address": null, + "recipient_detail_address": null, + "delivery_memo": null, + "delivery_memo_label": null, + "options": [], + "shipping_address": null, + "promotions_applied_snapshot": null, + "shipping_policy_applied_snapshot": null, + "admin_memo": null, + "customer_memo": null, + "created_at": "2026-07-08T01:44:49+00:00", + "updated_at": "2026-07-08T01:44:49+00:00", + "is_owner": true, + "abilities": { + "can_read": true, + "can_update": true, + "can_cancel": true + } + } + } +} +``` **에러 응답** @@ -918,9 +2075,256 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/orders/APIDOC-20260708-000001 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `4` | 기본 키 (내부 식별자) | +| order_number | string | `APIDOC-20260708-000001` | 주문번호 | +| base_currency | string | `KRW` | 금액 표기 기준 통화 (모든 *_formatted 필드의 통화, 주문 시점 base_currency 고정) | +| payment_currency | string | `KRW` | 결제 통화 (유저가 선택·결제한 통화, base_currency 와 다르면 병기 표시) | +| is_cross_currency | boolean | `false` | cross currency 여부 | +| order_status | string | `pending_payment` | 주문상태 (OrderStatusEnum) | +| order_status_label | string | `결제대기` | `order_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| order_status_variant | string | `warning` | `order_status` 값의 표시 변형 키 (UI 배지 색상/스타일) | +| is_partially_cancelled | boolean | `false` | partially cancelled 여부 | +| order_device | string | `pc` | 주문 디바이스 (pc/mobile/app) | +| order_device_label | string | `PC` | `order_device` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| is_first_order | boolean | `false` | first order 여부 | +| subtotal_amount | integer | `324327` | 상품 합계 (할인 전, 상품가×수량 합계) | +| subtotal_amount_formatted | string | `324,327원` | `subtotal_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_discount_amount | integer | `0` | 총 할인금액 (모든 할인 합계) | +| total_discount_amount_formatted | string | `0원` | `total_discount_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_shipping_amount | integer | `3000` | 총 배송비 | +| total_shipping_amount_formatted | string | `3,000원` | `total_shipping_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_amount | integer | `327327` | 최종 주문금액 (subtotal - discount + shipping) | +| total_amount_formatted | string | `327,327원` | `total_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_paid_amount | integer | `0` | 총 실제 결제금액 (PG 결제액) | +| total_paid_amount_formatted | string | `0원` | `total_paid_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_due_amount | integer | `327327` | 총 결제예정금액 (무통장 등) | +| total_due_amount_formatted | string | `327,327원` | `total_due_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| depositor_name | null | `null` | 무통장 입금자명 (입금확인 모달 기본값, payment 관계 로드 시에만 노출) | +| total_cancelled_amount | integer | `0` | 총 취소금액 | +| total_cancelled_amount_formatted | string | `0원` | `total_cancelled_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_refunded_amount | integer | `0` | 총 환불금액 | +| total_refunded_amount_formatted | string | `0원` | `total_refunded_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_refunded_points_amount | integer | `0` | 총 환불 포인트 | +| total_refunded_points_amount_formatted | string | `0원` | `total_refunded_points_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_product_coupon_discount_amount | integer | `0` | 상품 쿠폰 할인 합계 | +| total_product_coupon_discount_amount_formatted | string | `0원` | `total_product_coupon_discount_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_order_coupon_discount_amount | integer | `0` | 주문 쿠폰 할인 합계 | +| total_order_coupon_discount_amount_formatted | string | `0원` | `total_order_coupon_discount_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_coupon_discount_amount | integer | `0` | 총 쿠폰 할인금액 | +| total_coupon_discount_amount_formatted | string | `0원` | `total_coupon_discount_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_code_discount_amount | integer | `0` | 총 할인코드 할인금액 | +| total_code_discount_amount_formatted | string | `0원` | `total_code_discount_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_points_used_amount | integer | `0` | 총 포인트 사용액 | +| total_points_used_amount_formatted | string | `0원` | `total_points_used_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_deposit_used_amount | integer | `0` | 총 예치금 사용액 | +| total_deposit_used_amount_formatted | string | `0원` | `total_deposit_used_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_earned_points_amount | integer | `3273` | 총 적립 예정 포인트 | +| total_earned_points_amount_formatted | string | `3,273원` | `total_earned_points_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| mc_subtotal_amount | array | `[]` | 상품합계 다중 통화 | +| mc_total_discount_amount | array | `[]` | 총 할인 다중 통화 | +| mc_total_shipping_amount | array | `[]` | 총 배송비 다중 통화 | +| mc_total_amount | array | `[]` | 최종금액 다중 통화 (payment_amount) | +| mc_total_product_coupon_discount_amount | array | `[]` | 상품 쿠폰 할인 다중 통화 | +| mc_total_order_coupon_discount_amount | array | `[]` | 주문 쿠폰 할인 다중 통화 | +| mc_total_coupon_discount_amount | array | `[]` | 쿠폰 할인 합계 다중 통화 | +| mc_total_code_discount_amount | array | `[]` | 할인코드 할인 다중 통화 | +| mc_total_points_used_amount | array | `[]` | 포인트 사용 다중 통화 | +| mc_total_deposit_used_amount | array | `[]` | 예치금 사용 다중 통화 | +| item_count | integer | `4` | item 개수 (집계) | +| total_quantity | integer | `0` | 주문 옵션 수량 합계 (options 로드 시) | +| total_list_price | integer | `0` | 정가 합계 (옵션 스냅샷 정가 × 수량 합계) | +| total_list_price_formatted | string | `0원` | `total_list_price` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| ordered_at | string | `2026-07-08T01:44:49+00:00` | ordered 일시 | +| ordered_at_formatted | string | `2026-07-08 10:44:49` | `ordered_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| paid_at | null | `null` | paid 일시 | +| paid_at_formatted | null | `null` | `paid_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| confirmed_at | null | `null` | confirmed 일시 | +| confirmed_at_formatted | null | `null` | `confirmed_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| cancelled_at | null | `null` | cancelled 일시 | +| cancelled_at_formatted | null | `null` | `cancelled_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| delivered_at | null | `null` | delivered 일시 | +| total_tax_amount | integer | `29757` | 총 과세금액 | +| total_tax_amount_formatted | string | `29,757원` | `total_tax_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_vat_amount | integer | `0` | 총 부가세금액 | +| total_vat_amount_formatted | string | `0원` | `total_vat_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_taxable_supply_amount | integer | `29757` | 과세 공급가액 (총 과세금액 − 부가세, 영수증 과세금액 표시 SSoT) | +| total_taxable_supply_amount_formatted | string | `29,757원` | `total_taxable_supply_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_tax_free_amount | integer | `0` | 총 면세금액 | +| total_tax_free_amount_formatted | string | `0원` | `total_tax_free_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| user | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 대상 사용자 정보 객체 (uuid/name/email 등 — user 관계 파생) | +| user_id | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | user 식별자 (연관 리소스 참조) | +| user_login_id | null | `null` | 회원 로그인 아이디 (login_id, 비회원 주문이면 null) | +| orderer_name | null | `null` | 주문자 이름 (배송지에서 플래튼) | +| orderer_phone | null | `null` | 주문자 휴대전화 (배송지에서 플래튼) | +| orderer_tel | null | `null` | 주문자 일반전화 (배송지에서 플래튼, 미입력 시 null) | +| orderer_email | null | `null` | 주문자 이메일 (배송지에서 플래튼, 비회원 알림 수신 통로) | +| recipient_name | null | `null` | 수령인 이름 (배송지에서 플래튼) | +| recipient_phone | null | `null` | 수령인 휴대전화 (배송지에서 플래튼) | +| recipient_tel | null | `null` | 수령인 일반전화 (배송지에서 플래튼, 미입력 시 null) | +| recipient_zipcode | null | `null` | 수령인 우편번호 (배송지에서 플래튼) | +| recipient_address | null | `null` | 수령인 기본 주소 (배송지에서 플래튼) | +| recipient_detail_address | null | `null` | 수령인 상세 주소 (배송지에서 플래튼, 미입력 시 null) | +| delivery_memo | null | `null` | 배송 메모 (배송지에서 플래튼, 미입력 시 null) | +| delivery_memo_label | null | `null` | `delivery_memo` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| options | array | `[]` | 주문 옵션(품목) 목록 (OrderOptionResource — 상품·옵션·수량·옵션상태·금액) | +| shipping_address | null | `null` | 배송지 상세 (OrderAddressResource — 주문자/수령인/국내·해외 주소) | +| billing_address | null | `null` | 청구지 상세 (OrderAddressResource, 미분리 시 null) | +| payment | null | `null` | 대표 결제 정보 (OrderPaymentResource — 결제수단·결제상태·금액) | +| payments | array | `[]` | 결제 이력 목록 (OrderPaymentResource 배열 — 다회 결제/부분결제 포함) | +| shippings | array | `[]` | 배송 이력 목록 (OrderShippingResource 배열 — 배송유형·택배사·송장번호) | +| cancels | array | `[]` | 취소 이력 목록 (OrderCancelResource 배열 — 취소 사유·상세·취소일시, 최근순) | +| promotions_applied_snapshot | null | `null` | 적용된 프로모션 스냅샷 (재계산용) | +| shipping_policy_applied_snapshot | null | `null` | 적용된 배송정책 스냅샷 (재계산용) | +| admin_memo | null | `null` | 관리자 메모 (내부 관리용) | +| customer_memo | null | `null` | 고객 메모 (주문 시 고객이 남긴 메모) | +| created_at | string | `2026-07-08T01:44:49+00:00` | 생성 일시 | +| updated_at | string | `2026-07-08T01:44:49+00:00` | 최종 수정 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_update":true,"can_cancel":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "주문 정보를 조회했습니다.", + "data": { + "id": 4, + "order_number": "APIDOC-20260708-000001", + "base_currency": "KRW", + "payment_currency": "KRW", + "is_cross_currency": false, + "order_status": "pending_payment", + "order_status_label": "결제대기", + "order_status_variant": "warning", + "is_partially_cancelled": false, + "order_device": "pc", + "order_device_label": "PC", + "is_first_order": false, + "subtotal_amount": 324327, + "subtotal_amount_formatted": "324,327원", + "total_discount_amount": 0, + "total_discount_amount_formatted": "0원", + "total_shipping_amount": 3000, + "total_shipping_amount_formatted": "3,000원", + "total_amount": 327327, + "total_amount_formatted": "327,327원", + "total_paid_amount": 0, + "total_paid_amount_formatted": "0원", + "total_due_amount": 327327, + "total_due_amount_formatted": "327,327원", + "depositor_name": null, + "total_cancelled_amount": 0, + "total_cancelled_amount_formatted": "0원", + "total_refunded_amount": 0, + "total_refunded_amount_formatted": "0원", + "total_refunded_points_amount": 0, + "total_refunded_points_amount_formatted": "0원", + "total_product_coupon_discount_amount": 0, + "total_product_coupon_discount_amount_formatted": "0원", + "total_order_coupon_discount_amount": 0, + "total_order_coupon_discount_amount_formatted": "0원", + "total_coupon_discount_amount": 0, + "total_coupon_discount_amount_formatted": "0원", + "total_code_discount_amount": 0, + "total_code_discount_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": 3273, + "total_earned_points_amount_formatted": "3,273원", + "mc_subtotal_amount": [], + "mc_total_discount_amount": [], + "mc_total_shipping_amount": [], + "mc_total_amount": [], + "mc_total_product_coupon_discount_amount": [], + "mc_total_order_coupon_discount_amount": [], + "mc_total_coupon_discount_amount": [], + "mc_total_code_discount_amount": [], + "mc_total_points_used_amount": [], + "mc_total_deposit_used_amount": [], + "item_count": 4, + "total_quantity": 0, + "total_list_price": 0, + "total_list_price_formatted": "0원", + "ordered_at": "2026-07-08T01:44:49+00:00", + "ordered_at_formatted": "2026-07-08 10:44:49", + "paid_at": null, + "paid_at_formatted": null, + "confirmed_at": null, + "confirmed_at_formatted": null, + "cancelled_at": null, + "cancelled_at_formatted": null, + "delivered_at": null, + "total_tax_amount": 29757, + "total_tax_amount_formatted": "29,757원", + "total_vat_amount": 0, + "total_vat_amount_formatted": "0원", + "total_taxable_supply_amount": 29757, + "total_taxable_supply_amount_formatted": "29,757원", + "total_tax_free_amount": 0, + "total_tax_free_amount_formatted": "0원", + "user": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" + }, + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "user_login_id": null, + "orderer_name": null, + "orderer_phone": null, + "orderer_tel": null, + "orderer_email": null, + "recipient_name": null, + "recipient_phone": null, + "recipient_tel": null, + "recipient_zipcode": null, + "recipient_address": null, + "recipient_detail_address": null, + "delivery_memo": null, + "delivery_memo_label": null, + "options": [], + "shipping_address": null, + "billing_address": null, + "payment": null, + "payments": [], + "shippings": [], + "cancels": [], + "promotions_applied_snapshot": null, + "shipping_policy_applied_snapshot": null, + "admin_memo": null, + "customer_memo": null, + "created_at": "2026-07-08T01:44:49+00:00", + "updated_at": "2026-07-08T01:44:49+00:00", + "is_owner": true, + "abilities": { + "can_read": true, + "can_update": true, + "can_cancel": true + } + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/payments.md b/modules/_bundled/sirsoft-ecommerce/docs/api/payments.md index 17f1dde5..97a7316d 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/payments.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/payments.md @@ -29,10 +29,22 @@ | --- | --- | --- | --- | --- | --- | | provider | path | string | 예 | — | 대상 provider의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/payments/client-config/{provider} HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/presets.md b/modules/_bundled/sirsoft-ecommerce/docs/api/presets.md index 35d8d1b8..5cb25e37 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/presets.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/presets.md @@ -31,12 +31,35 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.search_preset.list_validation_rules`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/presets?target_screen=products HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-ecommerce::messages.presets.fetch_success", + "data": [] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -65,10 +88,32 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.preset.store_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/presets HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "target_screen": "예시값", + "name": "예시 이름", + "conditions": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -93,10 +138,23 @@ | --- | --- | --- | --- | --- | --- | | preset | path | string | 예 | — | 대상 preset의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/presets/{preset} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -126,10 +184,32 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.preset.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/presets/{preset} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": "예시 이름", + "conditions": [ + "예시값" + ], + "sort_order": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/product-common-infos.md b/modules/_bundled/sirsoft-ecommerce/docs/api/product-common-infos.md index 311acdde..09bb1d2e 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/product-common-infos.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/product-common-infos.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/product-common-infos HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -49,6 +58,61 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | language_count | integer | `2` | language 개수 (집계) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "공통정보 목록을 불러왔습니다.", + "data": { + "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 + } + } + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -80,10 +144,37 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-common-info.create_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/product-common-infos HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "content": [ + "예시 내용입니다." + ], + "content_mode": "text", + "is_default": true, + "is_active": true, + "sort_order": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -109,9 +200,40 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/product-common-infos/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| common_info_id | integer | `1` | common info 식별자 (연관 리소스 참조) | +| products_count | integer | `0` | products 개수 (집계) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "공통정보가 삭제되었습니다.", + "data": { + "common_info_id": 1, + "products_count": 0 + } +} +``` **에러 응답** @@ -138,9 +260,73 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/product-common-infos/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 공통정보","en":"API Doc Sample Common Info"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `API 문서 샘플 공통정보` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| content | array | `[]` | 본문 내용 | +| localized_content | string | `` | `content` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| content_mode | string | `text` | 내용 모드 (text: 텍스트, html: HTML) | +| is_default | boolean | `false` | default 여부 | +| is_active | boolean | `true` | active 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| icon | string | `info-circle` | 아이콘 식별자 (아이콘 클래스/이름) | +| created_at | string | `2026-07-08 10:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 | +| products_count | integer | `0` | products 개수 (집계) | +| language_count | integer | `0` | language 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```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 + } + } +} +``` **에러 응답** @@ -175,10 +361,37 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-common-info.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/product-common-infos/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "content": [ + "예시 내용입니다." + ], + "content_mode": "text", + "is_default": true, + "is_active": true, + "sort_order": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -205,9 +418,73 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/product-common-infos/1/toggle-active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 공통정보","en":"API Doc Sample Common Info"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `API 문서 샘플 공통정보` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| content | array | `[]` | 본문 내용 | +| localized_content | string | `` | `content` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| content_mode | string | `text` | 내용 모드 (text: 텍스트, html: HTML) | +| is_default | boolean | `false` | default 여부 | +| is_active | boolean | `false` | active 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| icon | string | `info-circle` | 아이콘 식별자 (아이콘 클래스/이름) | +| created_at | string | `2026-07-08 10:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 15:00:26` | 최종 수정 일시 | +| products_count | integer | `0` | products 개수 (집계) | +| language_count | integer | `0` | language 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```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": false, + "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": 0, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/product-image.md b/modules/_bundled/sirsoft-ecommerce/docs/api/product-image.md index 53866d1c..8c499c56 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/product-image.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/product-image.md @@ -29,10 +29,22 @@ | --- | --- | --- | --- | --- | --- | | hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/product-image/{hash} HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/product-labels.md b/modules/_bundled/sirsoft-ecommerce/docs/api/product-labels.md index d7f9bda7..ac029b50 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/product-labels.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/product-labels.md @@ -33,6 +33,15 @@ | sort | query | string | 아니오 | `name_asc`, `name_desc`, `created_asc`, `created_desc` | 정렬 기준 (필드명, `-` 접두 시 내림차순) | | locale | query | string | 아니오 | `ko`, `en`, `fr`, `ja` | 로케일 코드 (표시 언어/지역) | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/product-labels?is_active=1&active_only=1&search=%EC%98%88%EC%8B%9C%EA%B0%92&sort=name_asc&locale=ko HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -49,6 +58,46 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | assignments_count | integer | `0` | assignments 개수 (집계) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "라벨 목록을 조회했습니다.", + "data": { + "data": [ + { + "id": 1, + "name": { + "ko": "API 문서 샘플 라벨", + "en": "API Doc Sample Label" + }, + "color": null, + "is_active": true, + "sort_order": 0, + "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 + } + } + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -77,10 +126,33 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | | sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/product-labels HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "color": "#4F46E5", + "is_active": true, + "sort_order": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -106,9 +178,40 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/product-labels/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| label_id | integer | `1` | label 식별자 (연관 리소스 참조) | +| products_count | integer | `0` | products 개수 (집계) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "라벨이 삭제되었습니다.", + "data": { + "label_id": 1, + "products_count": 0 + } +} +``` **에러 응답** @@ -135,9 +238,61 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/product-labels/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 라벨","en":"API Doc Sample Label"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| color | null | `null` | 색상 코드 (예: #FF5733) | +| is_active | boolean | `true` | active 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| created_at | string | `2026-07-08 10:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 | +| assignments_count | integer | `0` | assignments 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "라벨 정보를 조회했습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 라벨", + "en": "API Doc Sample Label" + }, + "color": null, + "is_active": true, + "sort_order": 0, + "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 + } + } +} +``` **에러 응답** @@ -168,10 +323,33 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | | sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/product-labels/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "color": "#4F46E5", + "is_active": true, + "sort_order": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -198,9 +376,61 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/product-labels/1/toggle-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 라벨","en":"API Doc Sample Label"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| color | null | `null` | 색상 코드 (예: #FF5733) | +| is_active | boolean | `false` | active 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| created_at | string | `2026-07-08 10:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 15:00:27` | 최종 수정 일시 | +| assignments_count | integer | `0` | assignments 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "라벨 상태가 변경되었습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 라벨", + "en": "API Doc Sample Label" + }, + "color": null, + "is_active": false, + "sort_order": 0, + "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 + } + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/product-notice-templates.md b/modules/_bundled/sirsoft-ecommerce/docs/api/product-notice-templates.md index 71843b2a..3f578d88 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/product-notice-templates.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/product-notice-templates.md @@ -32,6 +32,15 @@ | per_page | query | string | 아니오 | — | 페이지당 항목 수 | | page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/product-notice-templates?search=%EC%98%88%EC%8B%9C%EA%B0%92&active_only=1&per_page=%EC%98%88%EC%8B%9C%EA%B0%92&page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -51,6 +60,63 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품정보제공고시 목록을 불러왔습니다.", + "data": { + "data": [ + { + "id": 1, + "name": { + "ko": "API 문서 샘플 고시템플릿", + "en": "API Doc Sample Notice Template" + }, + "localized_name": "API 문서 샘플 고시템플릿", + "category": null, + "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 + } + } + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -82,10 +148,36 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-notice-template.create_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/product-notice-templates HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "category": "예시값", + "fields": [ + "예시값" + ], + "is_active": true, + "sort_order": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -111,9 +203,38 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/product-notice-templates/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| template_id | integer | `1` | template 식별자 (연관 리소스 참조) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품정보제공고시가 삭제되었습니다.", + "data": { + "template_id": 1 + } +} +``` **에러 응답** @@ -140,9 +261,72 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/product-notice-templates/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 고시템플릿","en":"API Doc Sample Notice Templ…` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `API 문서 샘플 고시템플릿` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| category | null | `null` | 품목 카테고리 | +| fields | array | `[{"label":"품명","value":"샘플"}]` | 필드 정의 JSON | +| fields_count | integer | `1` | fields 개수 (집계) | +| is_active | boolean | `true` | active 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| icon | string | `file-alt` | 아이콘 식별자 (아이콘 클래스/이름) | +| 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품정보제공고시 목록을 불러왔습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 고시템플릿", + "en": "API Doc Sample Notice Template" + }, + "localized_name": "API 문서 샘플 고시템플릿", + "category": null, + "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 + } + } +} +``` **에러 응답** @@ -176,10 +360,36 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-notice-template.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/product-notice-templates/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "category": "예시값", + "fields": [ + "예시값" + ], + "is_active": true, + "sort_order": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -206,9 +416,72 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/product-notice-templates/1/copy HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `2` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 고시템플릿 (복사)","en":"API Doc Sample Notice …` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `API 문서 샘플 고시템플릿 (복사)` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| category | null | `null` | 품목 카테고리 | +| fields | array | `[{"label":"품명","value":"샘플"}]` | 필드 정의 JSON | +| fields_count | integer | `1` | fields 개수 (집계) | +| is_active | boolean | `true` | active 여부 | +| sort_order | integer | `1` | 표시 정렬 순서 값 (작을수록 우선) | +| icon | string | `file-alt` | 아이콘 식별자 (아이콘 클래스/이름) | +| created_at | string | `2026-07-08 15:00:27` | 생성 일시 | +| updated_at | string | `2026-07-08 15:00:27` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 201 +``` + +```json +{ + "success": true, + "message": "상품정보제공고시가 복사되었습니다.", + "data": { + "id": 2, + "name": { + "ko": "API 문서 샘플 고시템플릿 (복사)", + "en": "API Doc Sample Notice Template (Copy)" + }, + "localized_name": "API 문서 샘플 고시템플릿 (복사)", + "category": null, + "fields": [ + { + "label": "품명", + "value": "샘플" + } + ], + "fields_count": 1, + "is_active": true, + "sort_order": 1, + "icon": "file-alt", + "created_at": "2026-07-08 15:00:27", + "updated_at": "2026-07-08 15:00:27", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -235,9 +508,72 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/product-notice-templates/1/toggle-active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 고시템플릿","en":"API Doc Sample Notice Templ…` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `API 문서 샘플 고시템플릿` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| category | null | `null` | 품목 카테고리 | +| fields | array | `[{"label":"품명","value":"샘플"}]` | 필드 정의 JSON | +| fields_count | integer | `1` | fields 개수 (집계) | +| is_active | boolean | `false` | active 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| icon | string | `file-alt` | 아이콘 식별자 (아이콘 클래스/이름) | +| 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품정보제공고시가 비활성화되었습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 고시템플릿", + "en": "API Doc Sample Notice Template" + }, + "localized_name": "API 문서 샘플 고시템플릿", + "category": null, + "fields": [ + { + "label": "품명", + "value": "샘플" + } + ], + "fields_count": 1, + "is_active": false, + "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 + } + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/products.md b/modules/_bundled/sirsoft-ecommerce/docs/api/products.md index 19243c4e..8464fbfa 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/products.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/products.md @@ -52,6 +52,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.list_validation_rules`, `sirsoft-ecommerce.product.list_validation_messages`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/products?search_field=all&search_keyword=%EC%98%88%EC%8B%9C%EA%B0%92&category_id=1&no_category=1&date_type=%EC%98%88%EC%8B%9C%EA%B0%92&start_date=2026-01-01&end_date=2026-01-01&sales_status=%EC%98%88%EC%8B%9C%EA%B0%92&display_status=%EC%98%88%EC%8B%9C%EA%B0%92&brand_id=1&no_brand=1&tax_status=%EC%98%88%EC%8B%9C%EA%B0%92&price_type=%EC%98%88%EC%8B%9C%EA%B0%92&min_price=1&max_price=1&min_stock=1&max_stock=1&shipping_policy_id=1&sort_by=created_at&sort_order=asc&per_page=1&page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -100,6 +109,412 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품 정보를 조회했습니다.", + "data": { + "data": [ + { + "number": 3, + "id": 3, + "name": { + "ko": "nisi et corrupti", + "en": "suscipit esse id" + }, + "name_localized": "nisi et corrupti", + "product_code": "PROD-YNAZ-3669", + "sku": "SKU-EOQY-1167", + "thumbnail_url": null, + "list_price": 22605, + "list_price_formatted": "22,605원", + "selling_price": 20118, + "selling_price_formatted": "20,118원", + "discount_rate": 11, + "multi_currency_list_price": { + "KRW": { + "price": 22605, + "formatted": "22,605원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 19.21, + "formatted": "$19.21", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 2599, + "formatted": "¥2,599", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 131.11, + "formatted": "元131.11", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 17.63, + "formatted": "€17.63", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 20118, + "formatted": "20,118원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.1, + "formatted": "$17.10", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 2313, + "formatted": "¥2,313", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 116.68, + "formatted": "元116.68", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 15.69, + "formatted": "€15.69", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "stock_quantity": 164, + "safe_stock_quantity": 8, + "is_below_safe_stock": false, + "option_stock_sum": 24, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "sales_status_variant": "success", + "display_status": "visible", + "display_status_label": "전시", + "display_status_variant": "success", + "categories": [], + "primary_category": null, + "categories_with_path": [], + "brand_name": null, + "shipping_policy_id": null, + "shipping_policy_name": null, + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "has_options": false, + "options_count": 1, + "options": [ + { + "id": 1, + "option_code": "OPT-IXWA-1855", + "option_values": [ + { + "key": { + "ko": "색상", + "en": "Color" + }, + "value": { + "ko": "흰색", + "en": "Blue" + } + }, + { + "key": { + "ko": "사이즈", + "en": "Size" + }, + "value": { + "ko": "M", + "en": "M" + } + } + ], + "option_values_localized": { + "색상": "흰색", + "사이즈": "M" + }, + "option_name": { + "ko": "흰색/M", + "en": "Blue/M" + }, + "option_name_localized": "흰색/M", + "price_adjustment": 1000, + "price_adjustment_formatted": "+1,000원", + "price_adjustment_type": "increase", + "list_price": 23605, + "list_price_formatted": "23,605원", + "selling_price": 21118, + "selling_price_formatted": "21,118원", + "final_price": 21118, + "final_price_formatted": "21,118원", + "multi_currency_list_price": { + "KRW": { + "price": 23605, + "formatted": "23,605원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 20.06, + "formatted": "$20.06", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 2714, + "formatted": "¥2,714", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 136.91, + "formatted": "元136.91", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 18.41, + "formatted": "€18.41", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 21118, + "formatted": "21,118원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.95, + "formatted": "$17.95", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 2428, + "formatted": "¥2,428", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 122.48, + "formatted": "元122.48", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 16.47, + "formatted": "€16.47", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "stock_quantity": 24, + "safe_stock_quantity": 11, + "is_below_safe_stock": false, + "is_sold_out": false, + "is_default": false, + "is_active": true, + "sku": "SKU-OPT-MMYU-1269", + "sort_order": 75 + } + ], + "review_count": 0, + "rating_avg": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "number": 2, + "id": 2, + "name": { + "ko": "repudiandae et et", + "en": "velit alias distinctio" + }, + "name_localized": "repudiandae et et", + "product_code": "PROD-XSMH-5438", + "sku": "SKU-XXSC-9588", + "thumbnail_url": null, + "list_price": 654947, + "list_price_formatted": "654,947원", + "selling_price": 465012, + "selling_price_formatted": "465,012원", + "discount_rate": 29, + "multi_currency_list_price": { + "KRW": { + "price": 654947, + "formatted": "654,947원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 556.7, + "formatted": "$556.70", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 75318, + "formatted": "¥75,318", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 3798.69, + "formatted": "元3,798.69", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 510.86, + "formatted": "€510.86", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 465012, + "formatted": "465,012원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 395.26, + "formatted": "$395.26", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 53476, + "formatted": "¥53,476", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 2697.07, + "formatted": "元2,697.07", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 362.71, + "formatted": "€362.71", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "stock_quantity": 161, + "safe_stock_quantity": 49, + "is_below_safe_stock": false, + "option_stock_sum": 0, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "sales_status_variant": "success", + "display_status": "visible", + "display_status_label": "전시", + "display_status_variant": "success", + "categories": [], + "primary_category": null, + "categories_with_path": [], + "brand_name": null, + "shipping_policy_id": null, + "shipping_policy_name": null, + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "has_options": false, + "options_count": 0, + "options": [], + "review_count": 0, + "rating_avg": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + "... (총 3건 중 2건 표시)" + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + }, + "statistics": { + "total": 3, + "sales_status": { + "on_sale": 3 + }, + "display_status": { + "visible": 3 + }, + "low_stock_count": 0, + "out_of_stock_count": 0 + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 3, + "from": 1, + "to": 3, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -166,10 +581,93 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.store_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/products HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "product_code": "예시값", + "sales_product_code": "예시값", + "sku": "예시값", + "category_ids": [ + "예시값" + ], + "primary_category_id": 1, + "brand_id": 1, + "list_price": 1, + "selling_price": 1, + "stock_quantity": 1, + "safe_stock_quantity": 1, + "sales_status": "예시값", + "display_status": "예시값", + "tax_status": "예시값", + "tax_rate": 1, + "shipping_policy_id": 1, + "common_info_id": 1, + "description": [ + "예시 내용입니다." + ], + "description_mode": "text", + "thumbnail_hash": "예시값", + "image_temp_key": "예시값", + "images": [ + "예시값" + ], + "meta_title": [ + "예시 제목" + ], + "meta_description": [ + "예시 내용입니다." + ], + "meta_keywords": [ + "예시값" + ], + "seo_sync_title": true, + "seo_sync_description": true, + "use_main_image_for_og": true, + "has_options": true, + "option_groups": [ + "예시값" + ], + "options": [ + "예시값" + ], + "additional_options": [ + "예시값" + ], + "notice_items": [ + "예시값" + ], + "label_assignments": [ + "예시값" + ], + "min_purchase_qty": 1, + "max_purchase_qty": 1, + "purchase_restriction": "none", + "allowed_roles": [ + "예시값" + ], + "barcode": "예시값", + "hs_code": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -200,10 +698,33 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.bulk_price_validation_rules`). +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/products/bulk-price HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "method": "increase", + "value": 1, + "unit": "won" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -233,10 +754,32 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.bulk_status_validation_rules`). +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/products/bulk-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "field": "sales_status", + "value": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -266,10 +809,32 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.bulk_stock_validation_rules`). +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/products/bulk-stock HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "method": "increase", + "value": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -301,10 +866,42 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.bulk_update_validation_rules`). +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/products/bulk-update HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "bulk_changes": [ + "예시값" + ], + "items": [ + "예시값" + ], + "option_bulk_changes": [ + "예시값" + ], + "option_items": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -330,9 +927,143 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | code | path | string | 예 | — | 대상 리소스의 코드 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/products/by-code/APIDOCSAMPLE01 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| 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 | `[]` | 소속 카테고리 목록 (각 항목: id·현지화 이름·대표 여부. categories 관계 eager load 시에만 채워짐) | +| category_ids | array | `[]` | category 식별자 배열 (연관 리소스 참조) | +| primary_category_id | null | `null` | primary category 식별자 (연관 리소스 참조) | +| brand_id | null | `null` | brand 식별자 (연관 리소스 참조) | +| list_price | integer | `703155` | 정가 (기본통화 기준) | +| selling_price | integer | `597682` | 판매가 (기본통화 기준) | +| discount_rate | integer | `15` | 할인율(%) (정가 대비 판매가 할인 비율, (1 - 판매가/정가) × 100) | +| stock_quantity | integer | `509` | 재고 수량 (옵션 있으면 옵션 합계) | +| safe_stock_quantity | integer | `38` | 안전재고 수량 | +| is_below_safe_stock | boolean | `false` | below safe stock 여부 | +| is_stock_consistent | boolean | `true` | stock consistent 여부 | +| sales_status | string | `on_sale` | 판매상태: on_sale(판매중), suspended(판매중지), sold_out(품절), coming_soon(출시예정) | +| sales_status_label | string | `판매중` | `sales_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| display_status | string | `visible` | 전시상태: visible(전시), hidden(숨김) | +| display_status_label | string | `전시` | `display_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| tax_status | string | `taxable` | 과세여부: taxable(과세), tax_free(면세) | +| tax_status_label | string | `과세` | `tax_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| tax_rate | string | `10.00` | 세율 (%) | +| shipping_policy_id | null | `null` | shipping policy 식별자 (연관 리소스 참조) | +| common_info_id | null | `null` | common info 식별자 (연관 리소스 참조) | +| description | object | `{"ko":"Tempore et non fugit eos et aperiam dolore. Aut es…` | 설명 (다국어 필드는 로케일별 값 객체) | +| description_localized | string | `Tempore et non fugit eos et aperiam d…` | `description` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| description_mode | string | `text` | 설명 모드: text(텍스트), html(HTML) | +| min_purchase_qty | integer | `1` | 최소 구매 수량 | +| max_purchase_qty | integer | `0` | 최대 구매 수량 (0=무제한) | +| purchase_restriction | string | `none` | 구매 제한: none(없음), restricted(제한) | +| allowed_roles | null | `null` | 구매 허용 역할 ID 배열 | +| barcode | null | `null` | 바코드 | +| hs_code | null | `null` | HS 코드 (관세 분류) | +| images | array | `[]` | 상품 이미지 목록 (각 항목: hash·url·alt_text·is_thumbnail·sort_order 등, images 관계 로드 시) | +| thumbnail_hash | null | `null` | 대표 이미지 해시 (썸네일로 지정된 이미지의 hash) | +| thumbnail_url | null | `null` | thumbnail URL | +| meta_title | null | `null` | SEO 제목 (다국어 JSON) | +| meta_description | null | `null` | SEO 설명 (다국어 JSON) | +| meta_keywords | null | `null` | SEO 키워드 (배열) | +| seo_sync_title | boolean | `true` | SEO 제목 동기화 여부 (1: 상품명으로 자동 채움, 0: 직접 입력 보존) | +| seo_sync_description | boolean | `true` | SEO 설명 동기화 여부 (1: 상품 설명으로 자동 채움, 0: 직접 입력 보존) | +| has_options | boolean | `false` | options 여부 | +| option_groups | array | `[]` | 옵션 그룹 정의: [{name: "색상", values: ["빨강", "파랑"]}] | +| options | array | `[]` | 옵션(SKU) 목록 (수정 폼 바인딩용, 각 옵션의 id·코드·옵션값·가격·재고 등) | +| 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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", + "categories": [], + "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": { + "ko": "Tempore et non fugit eos et aperiam dolore. Aut esse fugiat adipisci iure itaque nisi. Earum ab dignissimos ipsa harum dolor sunt voluptatem.\n\nEst voluptatibus in sed distinctio. Delectus occaecati et earum est rerum ex. Beatae ex aut autem impedit facere totam. Et tempore minus veniam doloribus maiores eveniet dolorem ut. Culpa sed inventore ipsam.\n\nFuga praesentium distinctio quia ducimus harum assumenda. Ipsa dolorum odio reiciendis quis id. Tempore facere molestiae et reprehenderit reiciendis dicta quos. Quae labore deserunt fuga quam beatae. Praesentium aut fuga ratione aliquid.", + "en": "Soluta et ut repellendus rem. Id ea quibusdam in. Doloribus saepe rem praesentium et quis et molestiae.\n\nDolor voluptas aut qui unde sed quia. Corrupti qui enim laborum maxime possimus quis. Odit ducimus doloremque qui officiis fugit rerum. Magni ea possimus quia commodi optio saepe. Recusandae itaque in voluptatum et.\n\nRerum enim velit est inventore. Ipsa laboriosam provident ullam." + }, + "description_localized": "Tempore et non fugit eos et aperiam dolore. Aut esse fugiat adipisci iure itaque nisi. Earum ab dignissimos ipsa harum dolor sunt voluptatem.\n\nEst voluptatibus in sed distinctio. Delectus occaecati et earum est rerum ex. Beatae ex aut autem impedit facere totam. Et tempore minus veniam doloribus maiores eveniet dolorem ut. Culpa sed inventore ipsam.\n\nFuga praesentium distinctio quia ducimus harum assumenda. Ipsa dolorum odio reiciendis quis id. Tempore facere molestiae et reprehenderit reiciendis dicta quos. Quae labore deserunt fuga quam beatae. Praesentium aut fuga ratione aliquid.", + "description_mode": "text", + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "purchase_restriction": "none", + "allowed_roles": null, + "barcode": null, + "hs_code": null, + "images": [], + "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 + } + } +} +``` **에러 응답** @@ -401,10 +1132,93 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/products/by-code/APIDOCSAMPLE01 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "product_code": "예시값", + "sales_product_code": "예시값", + "sku": "예시값", + "category_ids": [ + "예시값" + ], + "primary_category_id": 1, + "brand_id": 1, + "list_price": 1, + "selling_price": 1, + "stock_quantity": 1, + "safe_stock_quantity": 1, + "sales_status": "예시값", + "display_status": "예시값", + "tax_status": "예시값", + "tax_rate": 1, + "shipping_policy_id": 1, + "common_info_id": 1, + "description": [ + "예시 내용입니다." + ], + "description_mode": "text", + "thumbnail_hash": "예시값", + "image_temp_key": "예시값", + "images": [ + "예시값" + ], + "meta_title": [ + "예시 제목" + ], + "meta_description": [ + "예시 내용입니다." + ], + "meta_keywords": [ + "예시값" + ], + "seo_sync_title": true, + "seo_sync_description": true, + "use_main_image_for_og": true, + "has_options": true, + "option_groups": [ + "예시값" + ], + "options": [ + "예시값" + ], + "additional_options": [ + "예시값" + ], + "notice_items": [ + "예시값" + ], + "label_assignments": [ + "예시값" + ], + "min_purchase_qty": 1, + "max_purchase_qty": 1, + "purchase_restriction": "none", + "allowed_roles": [ + "예시값" + ], + "barcode": "예시값", + "hs_code": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -429,10 +1243,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/products/generate-code HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -462,10 +1289,43 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-image.filter_upload_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/products/images HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary +Content-Disposition: form-data; name="temp_key" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="collection" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="alt_text" + +예시값 +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -493,10 +1353,30 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-image.filter_reorder_validation_rules`). +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/products/images/reorder HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "order": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -522,10 +1402,23 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/products/images/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -551,9 +1444,147 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/products/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| 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 | `[]` | 소속 카테고리 목록 (각 항목: id·현지화 이름·대표 여부. categories 관계 eager load 시에만 채워짐) | +| category_ids | array | `[]` | category 식별자 배열 (연관 리소스 참조) | +| primary_category_id | null | `null` | primary category 식별자 (연관 리소스 참조) | +| brand_id | null | `null` | brand 식별자 (연관 리소스 참조) | +| list_price | integer | `703155` | 정가 (기본통화 기준) | +| selling_price | integer | `597682` | 판매가 (기본통화 기준) | +| discount_rate | integer | `15` | 할인율(%) (정가 대비 판매가 할인 비율, (1 - 판매가/정가) × 100) | +| stock_quantity | integer | `509` | 재고 수량 (옵션 있으면 옵션 합계) | +| safe_stock_quantity | integer | `38` | 안전재고 수량 | +| is_below_safe_stock | boolean | `false` | below safe stock 여부 | +| is_stock_consistent | boolean | `true` | stock consistent 여부 | +| sales_status | string | `on_sale` | 판매상태: on_sale(판매중), suspended(판매중지), sold_out(품절), coming_soon(출시예정) | +| sales_status_label | string | `판매중` | `sales_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| display_status | string | `visible` | 전시상태: visible(전시), hidden(숨김) | +| display_status_label | string | `전시` | `display_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| tax_status | string | `taxable` | 과세여부: taxable(과세), tax_free(면세) | +| tax_status_label | string | `과세` | `tax_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| tax_rate | string | `10.00` | 세율 (%) | +| shipping_policy_id | null | `null` | shipping policy 식별자 (연관 리소스 참조) | +| common_info_id | null | `null` | common info 식별자 (연관 리소스 참조) | +| description | object | `{"ko":"Tempore et non fugit eos et aperiam dolore. Aut es…` | 설명 (다국어 필드는 로케일별 값 객체) | +| description_localized | string | `Tempore et non fugit eos et aperiam d…` | `description` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| description_mode | string | `text` | 설명 모드: text(텍스트), html(HTML) | +| min_purchase_qty | integer | `1` | 최소 구매 수량 | +| max_purchase_qty | integer | `0` | 최대 구매 수량 (0=무제한) | +| purchase_restriction | string | `none` | 구매 제한: none(없음), restricted(제한) | +| allowed_roles | null | `null` | 구매 허용 역할 ID 배열 | +| barcode | null | `null` | 바코드 | +| hs_code | null | `null` | HS 코드 (관세 분류) | +| label_assignments | array | `[]` | 라벨 할당 목록 (각 항목: label_id + 노출 시작/종료일) | +| notice_items | null | `null` | 상품정보제공고시 항목 목록 (각 항목: 항목명·내용 다국어) | +| images | array | `[]` | 상품 이미지 목록 (각 항목: hash·url·alt_text·is_thumbnail·sort_order 등, images 관계 로드 시) | +| thumbnail_hash | null | `null` | 대표 이미지 해시 (썸네일로 지정된 이미지의 hash) | +| thumbnail_url | null | `null` | thumbnail URL | +| meta_title | null | `null` | SEO 제목 (다국어 JSON) | +| meta_description | null | `null` | SEO 설명 (다국어 JSON) | +| meta_keywords | null | `null` | SEO 키워드 (배열) | +| seo_sync_title | boolean | `true` | SEO 제목 동기화 여부 (1: 상품명으로 자동 채움, 0: 직접 입력 보존) | +| seo_sync_description | boolean | `true` | SEO 설명 동기화 여부 (1: 상품 설명으로 자동 채움, 0: 직접 입력 보존) | +| has_options | boolean | `false` | options 여부 | +| option_groups | array | `[]` | 옵션 그룹 정의: [{name: "색상", values: ["빨강", "파랑"]}] | +| options | array | `[]` | 옵션(SKU) 목록 (수정 폼 바인딩용, 각 옵션의 id·코드·옵션값·가격·재고 등) | +| 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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", + "categories": [], + "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": { + "ko": "Tempore et non fugit eos et aperiam dolore. Aut esse fugiat adipisci iure itaque nisi. Earum ab dignissimos ipsa harum dolor sunt voluptatem.\n\nEst voluptatibus in sed distinctio. Delectus occaecati et earum est rerum ex. Beatae ex aut autem impedit facere totam. Et tempore minus veniam doloribus maiores eveniet dolorem ut. Culpa sed inventore ipsam.\n\nFuga praesentium distinctio quia ducimus harum assumenda. Ipsa dolorum odio reiciendis quis id. Tempore facere molestiae et reprehenderit reiciendis dicta quos. Quae labore deserunt fuga quam beatae. Praesentium aut fuga ratione aliquid.", + "en": "Soluta et ut repellendus rem. Id ea quibusdam in. Doloribus saepe rem praesentium et quis et molestiae.\n\nDolor voluptas aut qui unde sed quia. Corrupti qui enim laborum maxime possimus quis. Odit ducimus doloremque qui officiis fugit rerum. Magni ea possimus quia commodi optio saepe. Recusandae itaque in voluptatum et.\n\nRerum enim velit est inventore. Ipsa laboriosam provident ullam." + }, + "description_localized": "Tempore et non fugit eos et aperiam dolore. Aut esse fugiat adipisci iure itaque nisi. Earum ab dignissimos ipsa harum dolor sunt voluptatem.\n\nEst voluptatibus in sed distinctio. Delectus occaecati et earum est rerum ex. Beatae ex aut autem impedit facere totam. Et tempore minus veniam doloribus maiores eveniet dolorem ut. Culpa sed inventore ipsam.\n\nFuga praesentium distinctio quia ducimus harum assumenda. Ipsa dolorum odio reiciendis quis id. Tempore facere molestiae et reprehenderit reiciendis dicta quos. Quae labore deserunt fuga quam beatae. Praesentium aut fuga ratione aliquid.", + "description_mode": "text", + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "purchase_restriction": "none", + "allowed_roles": null, + "barcode": null, + "hs_code": null, + "label_assignments": [], + "notice_items": null, + "images": [], + "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 + } + } +} +``` **에러 응답** @@ -586,10 +1617,43 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-image.filter_upload_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/products/1/images HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary +Content-Disposition: form-data; name="temp_key" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="collection" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="alt_text" + +예시값 +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -617,10 +1681,23 @@ _요청 파라미터 없음._ | productId | path | string | 예 | — | 대상 product의 식별자 | | imageId | path | string | 예 | — | 대상 image의 식별자 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/products/1/images/{imageId}/thumbnail HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -646,9 +1723,38 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | product | path | string | 예 | — | 대상 product의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/products/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| deleted | boolean | `true` | 삭제 성공 여부 (상품이 정상 삭제되면 true) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품이 삭제되었습니다.", + "data": { + "deleted": true + } +} +``` **에러 응답** @@ -717,10 +1823,93 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/products/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "product_code": "예시값", + "sales_product_code": "예시값", + "sku": "예시값", + "category_ids": [ + "예시값" + ], + "primary_category_id": 1, + "brand_id": 1, + "list_price": 1, + "selling_price": 1, + "stock_quantity": 1, + "safe_stock_quantity": 1, + "sales_status": "예시값", + "display_status": "예시값", + "tax_status": "예시값", + "tax_rate": 1, + "shipping_policy_id": 1, + "common_info_id": 1, + "description": [ + "예시 내용입니다." + ], + "description_mode": "text", + "thumbnail_hash": "예시값", + "image_temp_key": "예시값", + "images": [ + "예시값" + ], + "meta_title": [ + "예시 제목" + ], + "meta_description": [ + "예시 내용입니다." + ], + "meta_keywords": [ + "예시값" + ], + "seo_sync_title": true, + "seo_sync_description": true, + "use_main_image_for_og": true, + "has_options": true, + "option_groups": [ + "예시값" + ], + "options": [ + "예시값" + ], + "additional_options": [ + "예시값" + ], + "notice_items": [ + "예시값" + ], + "label_assignments": [ + "예시값" + ], + "min_purchase_qty": 1, + "max_purchase_qty": 1, + "purchase_restriction": "none", + "allowed_roles": [ + "예시값" + ], + "barcode": "예시값", + "hs_code": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -747,6 +1936,15 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | product | path | string | 예 | — | 대상 product의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/products/1/can-delete HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -757,6 +1955,30 @@ _단건 응답: `data` 객체의 필드._ | reason | string | `이 상품은 5건의 주문 이력이 있어 삭제할 수 없습니다.` | 삭제 불가 사유 (canDelete=false 일 때 안내 문구) | | relatedData | object | `{"orders":5,"images":4,"options":3,"additionalOptions":0,…` | 연관 데이터 건수 (orders/images/options 등 상품에 연결된 하위 데이터 수) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "삭제 가능 여부를 확인했습니다.", + "data": { + "canDelete": true, + "reason": null, + "relatedData": { + "orders": 0, + "images": 0, + "options": 0, + "additionalOptions": 0, + "labelAssignments": 0 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -782,6 +2004,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | product | path | string | 예 | — | 대상 product의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/products/1/copy HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -827,6 +2058,64 @@ _단건 응답: `data` 객체의 필드._ | thumbnail_url | string | `/api/modules/sirsoft-ecommerce/produc…` | thumbnail URL | | categories | array | `[{"id":114,"name":{"ko":"스포츠","en":"Sports"},"name_locali…` | 소속 카테고리 목록 (breadcrumb 포함 — 복사 폼의 카테고리 표시용) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품 정보를 조회했습니다.", + "data": { + "name": { + "ko": "API 문서 샘플 상품", + "en": "API Doc Sample Product" + }, + "product_code": "WLA9NKCCGO7026T5", + "sales_product_code": null, + "sku": "SKU-DKOK-1319", + "brand_id": null, + "category_ids": [], + "primary_category_id": null, + "list_price": "703155.00", + "selling_price": "597682.00", + "stock_quantity": 509, + "safe_stock_quantity": 38, + "tax_status": "taxable", + "sales_status": "on_sale", + "display_status": "visible", + "options": [], + "additional_options": [], + "images": [], + "thumbnail_hash": null, + "description": { + "ko": "Tempore et non fugit eos et aperiam dolore. Aut esse fugiat adipisci iure itaque nisi. Earum ab dignissimos ipsa harum dolor sunt voluptatem.\n\nEst voluptatibus in sed distinctio. Delectus occaecati et earum est rerum ex. Beatae ex aut autem impedit facere totam. Et tempore minus veniam doloribus maiores eveniet dolorem ut. Culpa sed inventore ipsam.\n\nFuga praesentium distinctio quia ducimus harum assumenda. Ipsa dolorum odio reiciendis quis id. Tempore facere molestiae et reprehenderit reiciendis dicta quos. Quae labore deserunt fuga quam beatae. Praesentium aut fuga ratione aliquid.", + "en": "Soluta et ut repellendus rem. Id ea quibusdam in. Doloribus saepe rem praesentium et quis et molestiae.\n\nDolor voluptas aut qui unde sed quia. Corrupti qui enim laborum maxime possimus quis. Odit ducimus doloremque qui officiis fugit rerum. Magni ea possimus quia commodi optio saepe. Recusandae itaque in voluptatum et.\n\nRerum enim velit est inventore. Ipsa laboriosam provident ullam." + }, + "description_mode": "text", + "notice_items": null, + "shipping_policy_id": null, + "shipping_policy": null, + "common_info_id": null, + "label_assignments": [], + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "purchase_restriction": "none", + "allowed_roles": [], + "meta_title": null, + "meta_description": null, + "seo_tags": [], + "seo_sync_title": true, + "seo_sync_description": true, + "barcode": null, + "hs_code": null, + "thumbnail_url": null + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -852,6 +2141,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | product | path | string | 예 | — | 대상 product의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/products/1/form HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -898,6 +2196,66 @@ _단건 응답: `data` 객체의 필드._ | barcode | null | `null` | 바코드 | | hs_code | null | `null` | HS 코드 (관세 분류) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품 정보를 조회했습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 상품", + "en": "API Doc Sample Product" + }, + "product_code": "APIDOCSAMPLE01", + "sales_product_code": null, + "sku": "SKU-DKOK-1319", + "brand_id": null, + "category_ids": [], + "primary_category_id": null, + "created_at": "2026-07-08 01:44:49", + "updated_at": "2026-07-08 01:44:49", + "list_price": "703155.00", + "selling_price": "597682.00", + "stock_quantity": 509, + "safe_stock_quantity": 38, + "tax_status": "taxable", + "sales_status": "on_sale", + "display_status": "visible", + "options": [], + "additional_options": [], + "images": [], + "thumbnail_hash": null, + "description": { + "ko": "Tempore et non fugit eos et aperiam dolore. Aut esse fugiat adipisci iure itaque nisi. Earum ab dignissimos ipsa harum dolor sunt voluptatem.\n\nEst voluptatibus in sed distinctio. Delectus occaecati et earum est rerum ex. Beatae ex aut autem impedit facere totam. Et tempore minus veniam doloribus maiores eveniet dolorem ut. Culpa sed inventore ipsam.\n\nFuga praesentium distinctio quia ducimus harum assumenda. Ipsa dolorum odio reiciendis quis id. Tempore facere molestiae et reprehenderit reiciendis dicta quos. Quae labore deserunt fuga quam beatae. Praesentium aut fuga ratione aliquid.", + "en": "Soluta et ut repellendus rem. Id ea quibusdam in. Doloribus saepe rem praesentium et quis et molestiae.\n\nDolor voluptas aut qui unde sed quia. Corrupti qui enim laborum maxime possimus quis. Odit ducimus doloremque qui officiis fugit rerum. Magni ea possimus quia commodi optio saepe. Recusandae itaque in voluptatum et.\n\nRerum enim velit est inventore. Ipsa laboriosam provident ullam." + }, + "description_mode": "text", + "notice_items": null, + "shipping_policy_id": null, + "shipping_policy": null, + "common_info_id": null, + "label_assignments": [], + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "purchase_restriction": "none", + "allowed_roles": [], + "meta_title": null, + "meta_description": null, + "seo_tags": [], + "seo_sync_title": true, + "seo_sync_description": true, + "barcode": null, + "hs_code": null + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -923,6 +2281,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | product | path | string | 예 | — | 대상 product의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/products/1/logs HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -950,6 +2317,57 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_read":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품 처리 이력을 조회했습니다.", + "data": { + "data": [], + "links": { + "first": "https://api.example.com/api/modules/sirsoft-ecommerce/admin/products/1/logs?page=1", + "last": "https://api.example.com/api/modules/sirsoft-ecommerce/admin/products/1/logs?page=1", + "prev": null, + "next": null + }, + "meta": { + "current_page": 1, + "from": null, + "last_page": 1, + "links": [ + { + "url": null, + "label": "pagination.previous", + "page": null, + "active": false + }, + { + "url": "https://api.example.com/api/modules/sirsoft-ecommerce/admin/products/1/logs?page=1", + "label": "1", + "page": 1, + "active": true + }, + { + "url": null, + "label": "pagination.next", + "page": null, + "active": false + } + ], + "path": "https://api.example.com/api/modules/sirsoft-ecommerce/admin/products/1/logs", + "per_page": 25, + "to": null, + "total": 0 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -984,6 +2402,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.public_list_validation_rules`, `sirsoft-ecommerce.product.public_list_validation_messages`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/products?category_id=1&category_slug=example-key&brand_id=1&search=%EC%98%88%EC%8B%9C%EA%B0%92&sort=latest&min_price=1&max_price=1&per_page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -1029,6 +2456,270 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품 정보를 조회했습니다.", + "data": { + "data": [ + { + "number": 3, + "id": 3, + "name": { + "ko": "nisi et corrupti", + "en": "suscipit esse id" + }, + "name_localized": "nisi et corrupti", + "product_code": "PROD-YNAZ-3669", + "sku": "SKU-EOQY-1167", + "thumbnail_url": null, + "list_price": 22605, + "list_price_formatted": "22,605원", + "selling_price": 20118, + "selling_price_formatted": "20,118원", + "discount_rate": 11, + "multi_currency_list_price": { + "KRW": { + "price": 22605, + "formatted": "22,605원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 19.21, + "formatted": "$19.21", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 2599, + "formatted": "¥2,599", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 131.11, + "formatted": "元131.11", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 17.63, + "formatted": "€17.63", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 20118, + "formatted": "20,118원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.1, + "formatted": "$17.10", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 2313, + "formatted": "¥2,313", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 116.68, + "formatted": "元116.68", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 15.69, + "formatted": "€15.69", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "stock_quantity": 164, + "safe_stock_quantity": 8, + "is_below_safe_stock": false, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "sales_status_variant": "success", + "display_status": "visible", + "display_status_label": "전시", + "display_status_variant": "success", + "categories": [], + "primary_category": null, + "categories_with_path": [], + "brand_name": null, + "shipping_policy_id": null, + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "has_options": false, + "labels": [], + "review_count": 0, + "rating_avg": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "number": 2, + "id": 2, + "name": { + "ko": "repudiandae et et", + "en": "velit alias distinctio" + }, + "name_localized": "repudiandae et et", + "product_code": "PROD-XSMH-5438", + "sku": "SKU-XXSC-9588", + "thumbnail_url": null, + "list_price": 654947, + "list_price_formatted": "654,947원", + "selling_price": 465012, + "selling_price_formatted": "465,012원", + "discount_rate": 29, + "multi_currency_list_price": { + "KRW": { + "price": 654947, + "formatted": "654,947원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 556.7, + "formatted": "$556.70", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 75318, + "formatted": "¥75,318", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 3798.69, + "formatted": "元3,798.69", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 510.86, + "formatted": "€510.86", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 465012, + "formatted": "465,012원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 395.26, + "formatted": "$395.26", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 53476, + "formatted": "¥53,476", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 2697.07, + "formatted": "元2,697.07", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 362.71, + "formatted": "€362.71", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "stock_quantity": 161, + "safe_stock_quantity": 49, + "is_below_safe_stock": false, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "sales_status_variant": "success", + "display_status": "visible", + "display_status_label": "전시", + "display_status_variant": "success", + "categories": [], + "primary_category": null, + "categories_with_path": [], + "brand_name": null, + "shipping_policy_id": null, + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "has_options": false, + "labels": [], + "review_count": 0, + "rating_avg": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + "... (총 3건 중 2건 표시)" + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 3, + "from": 1, + "to": 3, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1055,6 +2746,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.public_new_validation_rules`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/products/new?limit=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -1096,6 +2796,363 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품 정보를 조회했습니다.", + "data": [ + { + "id": 3, + "name": { + "ko": "nisi et corrupti", + "en": "suscipit esse id" + }, + "name_localized": "nisi et corrupti", + "product_code": "PROD-YNAZ-3669", + "sku": "SKU-EOQY-1167", + "thumbnail_url": null, + "list_price": 22605, + "list_price_formatted": "22,605원", + "selling_price": 20118, + "selling_price_formatted": "20,118원", + "discount_rate": 11, + "multi_currency_list_price": { + "KRW": { + "price": 22605, + "formatted": "22,605원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 19.21, + "formatted": "$19.21", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 2599, + "formatted": "¥2,599", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 131.11, + "formatted": "元131.11", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 17.63, + "formatted": "€17.63", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 20118, + "formatted": "20,118원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.1, + "formatted": "$17.10", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 2313, + "formatted": "¥2,313", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 116.68, + "formatted": "元116.68", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 15.69, + "formatted": "€15.69", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "stock_quantity": 164, + "safe_stock_quantity": 8, + "is_below_safe_stock": false, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "sales_status_variant": "success", + "display_status": "visible", + "display_status_label": "전시", + "display_status_variant": "success", + "categories": [], + "primary_category": null, + "categories_with_path": [], + "shipping_policy_id": null, + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "has_options": false, + "labels": [], + "review_count": 0, + "rating_avg": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 2, + "name": { + "ko": "repudiandae et et", + "en": "velit alias distinctio" + }, + "name_localized": "repudiandae et et", + "product_code": "PROD-XSMH-5438", + "sku": "SKU-XXSC-9588", + "thumbnail_url": null, + "list_price": 654947, + "list_price_formatted": "654,947원", + "selling_price": 465012, + "selling_price_formatted": "465,012원", + "discount_rate": 29, + "multi_currency_list_price": { + "KRW": { + "price": 654947, + "formatted": "654,947원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 556.7, + "formatted": "$556.70", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 75318, + "formatted": "¥75,318", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 3798.69, + "formatted": "元3,798.69", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 510.86, + "formatted": "€510.86", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 465012, + "formatted": "465,012원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 395.26, + "formatted": "$395.26", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 53476, + "formatted": "¥53,476", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 2697.07, + "formatted": "元2,697.07", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 362.71, + "formatted": "€362.71", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "stock_quantity": 161, + "safe_stock_quantity": 49, + "is_below_safe_stock": false, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "sales_status_variant": "success", + "display_status": "visible", + "display_status_label": "전시", + "display_status_variant": "success", + "categories": [], + "primary_category": null, + "categories_with_path": [], + "shipping_policy_id": null, + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "has_options": false, + "labels": [], + "review_count": 0, + "rating_avg": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 1, + "name": { + "ko": "API 문서 샘플 상품", + "en": "API Doc Sample Product" + }, + "name_localized": "API 문서 샘플 상품", + "product_code": "APIDOCSAMPLE01", + "sku": "SKU-DKOK-1319", + "thumbnail_url": null, + "list_price": 703155, + "list_price_formatted": "703,155원", + "selling_price": 597682, + "selling_price_formatted": "597,682원", + "discount_rate": 15, + "multi_currency_list_price": { + "KRW": { + "price": 703155, + "formatted": "703,155원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 597.68, + "formatted": "$597.68", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 80862, + "formatted": "¥80,862", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 4078.3, + "formatted": "元4,078.30", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 548.46, + "formatted": "€548.46", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 597682, + "formatted": "597,682원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 508.03, + "formatted": "$508.03", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 68733, + "formatted": "¥68,733", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 3466.56, + "formatted": "元3,466.56", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 466.19, + "formatted": "€466.19", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "stock_quantity": 509, + "safe_stock_quantity": 38, + "is_below_safe_stock": false, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "sales_status_variant": "success", + "display_status": "visible", + "display_status_label": "전시", + "display_status_variant": "success", + "categories": [], + "primary_category": null, + "categories_with_path": [], + "shipping_policy_id": null, + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "has_options": false, + "labels": [], + "review_count": 1, + "rating_avg": 5, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1122,6 +3179,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.public_popular_validation_rules`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/products/popular?limit=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -1163,6 +3229,363 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품 정보를 조회했습니다.", + "data": [ + { + "id": 2, + "name": { + "ko": "repudiandae et et", + "en": "velit alias distinctio" + }, + "name_localized": "repudiandae et et", + "product_code": "PROD-XSMH-5438", + "sku": "SKU-XXSC-9588", + "thumbnail_url": null, + "list_price": 654947, + "list_price_formatted": "654,947원", + "selling_price": 465012, + "selling_price_formatted": "465,012원", + "discount_rate": 29, + "multi_currency_list_price": { + "KRW": { + "price": 654947, + "formatted": "654,947원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 556.7, + "formatted": "$556.70", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 75318, + "formatted": "¥75,318", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 3798.69, + "formatted": "元3,798.69", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 510.86, + "formatted": "€510.86", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 465012, + "formatted": "465,012원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 395.26, + "formatted": "$395.26", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 53476, + "formatted": "¥53,476", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 2697.07, + "formatted": "元2,697.07", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 362.71, + "formatted": "€362.71", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "stock_quantity": 161, + "safe_stock_quantity": 49, + "is_below_safe_stock": false, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "sales_status_variant": "success", + "display_status": "visible", + "display_status_label": "전시", + "display_status_variant": "success", + "categories": [], + "primary_category": null, + "categories_with_path": [], + "shipping_policy_id": null, + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "has_options": false, + "labels": [], + "review_count": 0, + "rating_avg": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 1, + "name": { + "ko": "API 문서 샘플 상품", + "en": "API Doc Sample Product" + }, + "name_localized": "API 문서 샘플 상품", + "product_code": "APIDOCSAMPLE01", + "sku": "SKU-DKOK-1319", + "thumbnail_url": null, + "list_price": 703155, + "list_price_formatted": "703,155원", + "selling_price": 597682, + "selling_price_formatted": "597,682원", + "discount_rate": 15, + "multi_currency_list_price": { + "KRW": { + "price": 703155, + "formatted": "703,155원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 597.68, + "formatted": "$597.68", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 80862, + "formatted": "¥80,862", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 4078.3, + "formatted": "元4,078.30", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 548.46, + "formatted": "€548.46", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 597682, + "formatted": "597,682원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 508.03, + "formatted": "$508.03", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 68733, + "formatted": "¥68,733", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 3466.56, + "formatted": "元3,466.56", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 466.19, + "formatted": "€466.19", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "stock_quantity": 509, + "safe_stock_quantity": 38, + "is_below_safe_stock": false, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "sales_status_variant": "success", + "display_status": "visible", + "display_status_label": "전시", + "display_status_variant": "success", + "categories": [], + "primary_category": null, + "categories_with_path": [], + "shipping_policy_id": null, + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "has_options": false, + "labels": [], + "review_count": 1, + "rating_avg": 5, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + }, + { + "id": 3, + "name": { + "ko": "nisi et corrupti", + "en": "suscipit esse id" + }, + "name_localized": "nisi et corrupti", + "product_code": "PROD-YNAZ-3669", + "sku": "SKU-EOQY-1167", + "thumbnail_url": null, + "list_price": 22605, + "list_price_formatted": "22,605원", + "selling_price": 20118, + "selling_price_formatted": "20,118원", + "discount_rate": 11, + "multi_currency_list_price": { + "KRW": { + "price": 22605, + "formatted": "22,605원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 19.21, + "formatted": "$19.21", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 2599, + "formatted": "¥2,599", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 131.11, + "formatted": "元131.11", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 17.63, + "formatted": "€17.63", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 20118, + "formatted": "20,118원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 17.1, + "formatted": "$17.10", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 2313, + "formatted": "¥2,313", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 116.68, + "formatted": "元116.68", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 15.69, + "formatted": "€15.69", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "stock_quantity": 164, + "safe_stock_quantity": 8, + "is_below_safe_stock": false, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "sales_status_variant": "success", + "display_status": "visible", + "display_status_label": "전시", + "display_status_variant": "success", + "categories": [], + "primary_category": null, + "categories_with_path": [], + "shipping_policy_id": null, + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "has_options": false, + "labels": [], + "review_count": 0, + "rating_avg": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "is_owner": false, + "abilities": { + "can_update": true, + "can_delete": true + } + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1189,12 +3612,35 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.public_recent_validation_rules`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/products/recent?ids=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품 정보를 조회했습니다.", + "data": [] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1219,9 +3665,200 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/products/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| 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 | `[]` | 소속 카테고리 목록 (각 항목: id·현지화 이름·대표 여부. categories 관계 eager load 시에만 채워짐) | +| category_name | null | `null` | 대표 카테고리명 (is_primary 카테고리의 현지화 이름) | +| list_price | integer | `703155` | 정가 (기본통화 기준) | +| list_price_formatted | string | `703,155원` | `list_price` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| selling_price | integer | `597682` | 판매가 (기본통화 기준) | +| selling_price_formatted | string | `597,682원` | `selling_price` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| discount_rate | integer | `15` | 할인율(%) (정가 대비 판매가 할인 비율, (1 - 판매가/정가) × 100) | +| multi_currency_list_price | object | `{"KRW":{"price":703155,"formatted":"703,155원","is_default…` | 통화별 정가 맵 (통화코드 → {price, formatted, is_default, editable}, 설정된 모든 통화의 환산 정가) | +| multi_currency_selling_price | object | `{"KRW":{"price":597682,"formatted":"597,682원","is_default…` | 통화별 판매가 맵 (통화코드 → {price, formatted, is_default, editable}, 설정된 모든 통화의 환산 판매가) | +| stock_quantity | integer | `509` | 재고 수량 (옵션 있으면 옵션 합계) | +| min_purchase_qty | integer | `1` | 최소 구매 수량 | +| max_purchase_qty | integer | `0` | 최대 구매 수량 (0=무제한) | +| sales_status | string | `on_sale` | 판매상태: on_sale(판매중), suspended(판매중지), sold_out(품절), coming_soon(출시예정) | +| sales_status_label | string | `판매중` | `sales_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| brand_name | null | `null` | 브랜드명 (연관 브랜드의 현지화 이름) | +| labels | array | `[]` | 노출 중인 상품 라벨 목록 (각 항목: 라벨명·색상, 활성 라벨을 sort_order 순으로 정렬) | +| additional_options | array | `[]` | 추가옵션 그룹 목록 (각 그룹: 그룹명·필수 여부·선택지 목록, 활성 옵션을 sort_order 순으로 정렬) | +| shipping_policy_id | null | `null` | shipping policy 식별자 (연관 리소스 참조) | +| is_shippable_to_selected_country | boolean | `true` | shippable to selected country 여부 | +| selected_shipping_country | string | `KR` | 배송비 계산에 적용된 배송 국가 코드 (ResolveShippingCountry 해석 결과) | +| free_shipping | boolean | `false` | 무료배송 여부 (적용 배송정책의 청구방식이 무료(FREE)인 경우 true) | +| shipping_fee_formatted | string | `` | `shipping_fee` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| shipping_policy | null | `null` | 적용 배송정책 요약 객체 (정책명·청구방식·기본배송비·무료배송 기준액 등, 상품 정책 없으면 기본 정책으로 폴백) | +| short_description | null | `null` | 짧은 설명 (상품 목록/카드용 요약 설명, 다국어 필드는 로케일별 값 객체) | +| short_description_localized | null | `null` | `short description` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| description | object | `{"ko":"Tempore et non fugit eos et aperiam dolore. Aut es…` | 설명 (다국어 필드는 로케일별 값 객체) | +| description_localized | string | `Tempore et non fugit eos et aperiam d…` | `description` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| description_mode | string | `text` | 설명 모드: text(텍스트), html(HTML) | +| images | array | `[]` | 상품 이미지 목록 (각 항목: hash·url·alt_text·is_thumbnail·sort_order 등, images 관계 로드 시) | +| thumbnail_url | null | `null` | thumbnail URL | +| meta_title | null | `null` | SEO 제목 (다국어 JSON) | +| meta_description | null | `null` | SEO 설명 (다국어 JSON) | +| meta_keywords | null | `null` | SEO 키워드 (배열) | +| has_options | boolean | `false` | options 여부 | +| option_groups | array | `[]` | 옵션 그룹 정의: [{name: "색상", values: ["빨강", "파랑"]}] | +| options | array | `[]` | 활성 옵션(SKU) 목록 (각 옵션의 코드·옵션값·가격·재고 등, ProductOptionResource) | +| notice | null | `null` | 상품정보제공고시 (템플릿명 + 항목별 라벨/내용 목록, notice 관계 로드 시) | +| common_info | null | `null` | 공통정보 (name·content·content_mode, commonInfo 관계 로드 시 — 여러 상품이 공유하는 공통 안내문) | +| is_wishlisted | boolean | `false` | wishlisted 여부 | + +**응답 예시** + +```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", + "categories": [], + "category_name": null, + "list_price": 703155, + "list_price_formatted": "703,155원", + "selling_price": 597682, + "selling_price_formatted": "597,682원", + "discount_rate": 15, + "multi_currency_list_price": { + "KRW": { + "price": 703155, + "formatted": "703,155원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 597.68, + "formatted": "$597.68", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 80862, + "formatted": "¥80,862", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 4078.3, + "formatted": "元4,078.30", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 548.46, + "formatted": "€548.46", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "multi_currency_selling_price": { + "KRW": { + "price": 597682, + "formatted": "597,682원", + "is_default": true, + "editable": true + }, + "USD": { + "price": 508.03, + "formatted": "$508.03", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 68733, + "formatted": "¥68,733", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 3466.56, + "formatted": "元3,466.56", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 466.19, + "formatted": "€466.19", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "stock_quantity": 509, + "min_purchase_qty": 1, + "max_purchase_qty": 0, + "sales_status": "on_sale", + "sales_status_label": "판매중", + "brand_name": null, + "labels": [], + "additional_options": [], + "shipping_policy_id": null, + "is_shippable_to_selected_country": true, + "selected_shipping_country": "KR", + "free_shipping": false, + "shipping_fee_formatted": "", + "shipping_policy": null, + "short_description": null, + "short_description_localized": null, + "description": { + "ko": "Tempore et non fugit eos et aperiam dolore. Aut esse fugiat adipisci iure itaque nisi. Earum ab dignissimos ipsa harum dolor sunt voluptatem.\n\nEst voluptatibus in sed distinctio. Delectus occaecati et earum est rerum ex. Beatae ex aut autem impedit facere totam. Et tempore minus veniam doloribus maiores eveniet dolorem ut. Culpa sed inventore ipsam.\n\nFuga praesentium distinctio quia ducimus harum assumenda. Ipsa dolorum odio reiciendis quis id. Tempore facere molestiae et reprehenderit reiciendis dicta quos. Quae labore deserunt fuga quam beatae. Praesentium aut fuga ratione aliquid.", + "en": "Soluta et ut repellendus rem. Id ea quibusdam in. Doloribus saepe rem praesentium et quis et molestiae.\n\nDolor voluptas aut qui unde sed quia. Corrupti qui enim laborum maxime possimus quis. Odit ducimus doloremque qui officiis fugit rerum. Magni ea possimus quia commodi optio saepe. Recusandae itaque in voluptatum et.\n\nRerum enim velit est inventore. Ipsa laboriosam provident ullam." + }, + "description_localized": "Tempore et non fugit eos et aperiam dolore. Aut esse fugiat adipisci iure itaque nisi. Earum ab dignissimos ipsa harum dolor sunt voluptatem.\n\nEst voluptatibus in sed distinctio. Delectus occaecati et earum est rerum ex. Beatae ex aut autem impedit facere totam. Et tempore minus veniam doloribus maiores eveniet dolorem ut. Culpa sed inventore ipsam.\n\nFuga praesentium distinctio quia ducimus harum assumenda. Ipsa dolorum odio reiciendis quis id. Tempore facere molestiae et reprehenderit reiciendis dicta quos. Quae labore deserunt fuga quam beatae. Praesentium aut fuga ratione aliquid.", + "description_mode": "text", + "images": [], + "thumbnail_url": null, + "meta_title": null, + "meta_description": null, + "meta_keywords": null, + "has_options": false, + "option_groups": [], + "options": [], + "notice": null, + "common_info": null, + "is_wishlisted": false + } +} +``` **에러 응답** @@ -1247,9 +3884,72 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | productId | path | string | 예 | — | 대상 product의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/products/1/downloadable-coupons HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) - +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| coupon_id | integer | `1` | coupon 식별자 (연관 리소스 참조) | +| localized_name | string | `API 문서 샘플 쿠폰` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| benefit_formatted | string | `1,000원 할인` | `benefit` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| multi_currency_benefit_formatted | object | `{"KRW":"1,000원 할인","USD":"$0.85 할인","JPY":"¥115 할인","CNY"…` | `multi_currency_benefit` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| target_type | string | `order_amount` | 적용대상(할인 기준): product_amount(상품금액), order_amount(주문금액), shipping_fee(배송비) | +| target_type_short_label | string | `주문` | `target_type_short` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| valid_period_formatted | string | `-` | `valid_period` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| min_order_amount | string | `0.00` | 쿠폰 적용 최소 주문금액 (0=제한 없음) | +| min_order_amount_formatted | string | `0원` | `min_order_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| multi_currency_min_order_amount | null | `null` | 최소 주문금액의 통화별 환산 맵 (0이면 null) | +| total_quantity | null | `null` | 총 발급 수량 (null=무제한) | +| remaining_quantity | null | `null` | 잔여 발급 가능 수량 (total_quantity − issued_count, 무제한이면 null) | +| is_downloaded | boolean | `false` | downloaded 여부 | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "다운로드 가능한 쿠폰 목록을 불러왔습니다.", + "data": { + "data": [ + { + "coupon_id": 1, + "localized_name": "API 문서 샘플 쿠폰", + "benefit_formatted": "1,000원 할인", + "multi_currency_benefit_formatted": { + "KRW": "1,000원 할인", + "USD": "$0.85 할인", + "JPY": "¥115 할인", + "CNY": "元5.80 할인", + "EUR": "€0.78 할인" + }, + "target_type": "order_amount", + "target_type_short_label": "주문", + "valid_period_formatted": "-", + "min_order_amount": "0.00", + "min_order_amount_formatted": "0원", + "multi_currency_min_order_amount": null, + "total_quantity": null, + "remaining_quantity": null, + "is_downloaded": false + } + ] + } +} +``` **에러 응답** @@ -1275,9 +3975,58 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | productId | path | string | 예 | — | 대상 product의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/products/1/inquiries HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| items | array | `[]` | 상품문의 항목 배열 (각 항목: id·작성자·작성일·답변 여부·게시판 연동 시 제목/내용/비밀글 여부/답변/첨부) | +| meta | object | `{"board_settings":{"secret_mode":"disabled","categories":…` | 문의 목록 메타 (board_settings 게시판 설정, inquiry_available 문의 게시판 연동 여부, current_page/per_page/total/last_page 페이지네이션, abilities 답변·삭제 권한) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "문의 목록을 조회했습니다.", + "data": { + "items": [], + "meta": { + "board_settings": { + "secret_mode": "{MASKED}", + "categories": [], + "use_file_upload": false, + "max_file_count": 5, + "max_file_size": 10485760, + "allowed_extensions": [], + "min_title_length": 2, + "max_title_length": 200, + "min_content_length": 10, + "max_content_length": 10000 + }, + "inquiry_available": false, + "total": 0, + "current_page": 1, + "per_page": 25, + "last_page": 1 + } + } +} +``` **에러 응답** @@ -1310,10 +4059,32 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.inquiry.store_validation_rules`, `sirsoft-ecommerce.inquiry.store_validation_messages`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/products/1/inquiries HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +Content-Type: application/json + +{ + "title": "예시 제목", + "category": "예시값", + "content": "예시 내용입니다.", + "is_secret": true, + "temp_key": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -1347,9 +4118,138 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.review.public_list_validation_rules`, `sirsoft-ecommerce.review.public_list_validation_messages`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/products/1/reviews?sort=created_at_desc&photo_only=0&page=1&per_page=1&rating=1&option_filters=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| reviews | object | `{"data":[{"id":1,"product_id":1,"order_option_id":1,"user…` | 리뷰 목록 (data 배열 + 페이지네이션 메타, 공개 상태 리뷰만 정렬/필터 적용해 반환) | +| rating_stats | object | `{"5":{"count":1,"percent":100},"4":{"count":0,"percent":0…` | 별점 분포 통계 (별점(1~5)별 count·percent 맵 + avg 평균 별점, 공개 리뷰 기준) | +| option_filters | array | `[]` | 옵션 조건 필터 후보 (구매 옵션 키·값별 리뷰 건수 집계, 옵션별 리뷰 필터 UI용) | +| total_count | integer | `1` | total 개수 (집계) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "리뷰 목록을 조회했습니다.", + "data": { + "reviews": { + "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. Enim cum mollitia nisi. Adipisci sunt tenetur et tempora tempora eius rerum.", + "content_mode": "text", + "status": "visible", + "status_label": "전시중", + "status_badge_color": "blue", + "images": [], + "image_count": 0, + "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 10:44:49", + "abilities": { + "can_update": true, + "can_delete": true + } + } + ], + "links": { + "first": "https://api.example.com/api/modules/sirsoft-ecommerce/products/1/reviews?page=1", + "last": "https://api.example.com/api/modules/sirsoft-ecommerce/products/1/reviews?page=1", + "prev": null, + "next": null + }, + "meta": { + "current_page": 1, + "from": 1, + "last_page": 1, + "links": [ + { + "url": null, + "label": "pagination.previous", + "page": null, + "active": false + }, + { + "url": "https://api.example.com/api/modules/sirsoft-ecommerce/products/1/reviews?page=1", + "label": "1", + "page": 1, + "active": true + }, + { + "url": null, + "label": "pagination.next", + "page": null, + "active": false + } + ], + "path": "https://api.example.com/api/modules/sirsoft-ecommerce/products/1/reviews", + "per_page": 25, + "to": 1, + "total": 1 + } + }, + "rating_stats": { + "5": { + "count": 1, + "percent": 100 + }, + "4": { + "count": 0, + "percent": 0 + }, + "3": { + "count": 0, + "percent": 0 + }, + "2": { + "count": 0, + "percent": 0 + }, + "1": { + "count": 0, + "percent": 0 + }, + "avg": 5 + }, + "option_filters": [], + "total_count": 1 + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/promotion-coupons.md b/modules/_bundled/sirsoft-ecommerce/docs/api/promotion-coupons.md index f93ecfaa..fb590202 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/promotion-coupons.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/promotion-coupons.md @@ -49,6 +49,15 @@ | issue_end_date | query | date | 아니오 | — | 발급기간 범위 종료 (시작일 이후) | | created_by | query | uuid | 아니오 | — | 등록자(생성한 관리자) UUID 필터 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/promotion-coupons?page=1&per_page=1&sort_by=created_at&sort_order=asc&search_field=all&search_keyword=%EC%98%88%EC%8B%9C%EA%B0%92&target_type=all&discount_type=all&issue_status=all&issue_method=all&issue_condition=all&min_benefit_amount=1&max_benefit_amount=1&min_order_amount=1&created_start_date=2026-01-01&created_end_date=2026-01-01&valid_start_date=2026-01-01&valid_end_date=2026-01-01&issue_start_date=2026-01-01&issue_end_date=2026-01-01&created_by=9f8b2c1a-4d3e-4a2b-8c1d-0e1f2a3b4c5d HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -106,6 +115,131 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | issues_count | integer | `0` | issues 개수 (집계) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "쿠폰 목록을 조회했습니다.", + "data": { + "data": [ + { + "id": 1, + "name": { + "ko": "API 문서 샘플 쿠폰", + "en": "API Doc Sample Coupon" + }, + "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": 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 + }, + "USD": { + "price": 0.85, + "formatted": "$0.85", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 115, + "formatted": "¥115", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 5.8, + "formatted": "元5.80", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 0.78, + "formatted": "€0.78", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "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": null, + "valid_to": null, + "valid_period_formatted": "-", + "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": null, + "created_by_name": "-", + "created_by_email": null, + "creator": null, + "issues_count": 0, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -154,10 +288,57 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.coupon.create_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/promotion-coupons HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "description": [ + "예시 내용입니다." + ], + "target_type": "product_amount", + "discount_type": "fixed", + "discount_value": 1, + "discount_max_amount": 1, + "min_order_amount": 1, + "issue_method": "direct", + "issue_condition": "manual", + "issue_status": "issuing", + "total_quantity": 1, + "per_user_limit": 1, + "valid_type": "period", + "valid_days": 1, + "valid_from": "2026-01-01", + "valid_to": "2026-01-01", + "issue_from": "2026-01-01", + "issue_to": "2026-01-01", + "is_combinable": true, + "target_scope": "all", + "products": [ + "예시값" + ], + "categories": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -184,10 +365,31 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | | issue_status | body | string | 예 | `issuing`, `stopped` | 일괄 적용할 발급상태: issuing(발급중), stopped(발급중단) | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/promotion-coupons/bulk-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "issue_status": "issuing" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -213,9 +415,38 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/promotion-coupons/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| coupon_id | integer | `1` | coupon 식별자 (연관 리소스 참조) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "쿠폰이 삭제되었습니다.", + "data": { + "coupon_id": 1 + } +} +``` **에러 응답** @@ -242,9 +473,186 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/promotion-coupons/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 쿠폰","en":"API Doc Sample Coupon"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `API 문서 샘플 쿠폰` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| description | null | `null` | 설명 (다국어 필드는 로케일별 값 객체) | +| localized_description | null | `null` | `description` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| target_type | string | `order_amount` | 적용대상: product_amount(상품금액), order_amount(주문금액), shipping_fee(배송비) | +| target_type_label | string | `주문금액` | `target_type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| target_type_badge_color | string | `blue` | `target_type` 배지 색상 (상품금액=teal, 주문금액=blue, 배송비=orange) | +| discount_type | string | `fixed` | 혜택유형: fixed(정액), rate(정률) | +| discount_type_label | string | `정액할인` | `discount_type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| discount_value | integer | `1000` | 혜택값 (정액: 금액, 정률: %) | +| discount_max_amount | null | `null` | 최대 할인액 (정률 시) | +| min_order_amount | integer | `0` | 최소 주문금액 | +| benefit_formatted | string | `1,000원 할인` | `benefit` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| multi_currency_discount_value | object | `{"KRW":{"price":1000,"formatted":"1,000원","is_default":tr…` | 정액 할인 금액의 통화별 환산 맵 (정률은 통화 무관이라 null) | +| multi_currency_min_order_amount | null | `null` | 최소 주문금액의 통화별 환산 맵 (0이면 null) | +| multi_currency_discount_max_amount | null | `null` | 최대 할인액의 통화별 환산 맵 (미설정 시 null) | +| issue_method | string | `download` | 발급방법: direct(직접발급), download(다운로드), auto(자동발급) | +| issue_method_label | string | `다운로드` | `issue_method` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| 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` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| issue_condition_badge_color | string | `orange` | `issue_condition` 배지 색상 (수동=orange, 회원가입=blue, 첫구매=teal, 생일=pink) | +| issue_status | string | `issuing` | 발급상태: issuing(발급중), stopped(발급중단) | +| issue_status_label | string | `발급중` | `issue_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| issue_status_badge_color | string | `blue` | `issue_status` 배지 색상 (발급중=blue, 발급중단=orange) | +| total_quantity | null | `null` | 총 발급 수량 (NULL=무제한) | +| issued_count | integer | `0` | issued 개수 (집계) | +| per_user_limit | integer | `1` | 회원당 발급 제한 | +| issue_count_formatted | string | `0/무제한` | `issue_count` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| valid_type | string | `period` | 유효기간 유형: period(기간지정), days_from_issue(발급일로부터) | +| valid_days | null | `null` | 발급일로부터 N일 (valid_type=days_from_issue) | +| valid_from | null | `null` | 유효기간 시작 | +| valid_to | null | `null` | 유효기간 종료 | +| valid_period_formatted | string | `-` | `valid_period` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| issue_from | null | `null` | 발급기간 시작 | +| issue_to | null | `null` | 발급기간 종료 | +| issue_period_formatted | string | `상시발급` | `issue_period` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| is_combinable | boolean | `false` | combinable 여부 | +| target_scope | string | `all` | 적용 범위: all(전체), products(특정상품), categories(특정카테고리) | +| target_scope_label | string | `전체상품` | `target_scope` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| is_issuable | boolean | `true` | issuable 여부 | +| created_at | string | `2026-07-08 10:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 | +| created_by | null | `null` | 쿠폰 등록자(관리자) 식별자 (users 참조, 삭제 시 null) | +| created_by_name | string | `-` | 등록자 이름 (creator 미로드/미설정 시 `-`) | +| created_by_email | null | `null` | 등록자 이메일 (creator 관계 파생) | +| creator | null | `null` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| included_products | array | `[]` | 적용 대상 포함 상품 목록 (target_scope=products 시 이 상품에만 적용) | +| excluded_products | array | `[]` | 적용 제외 상품 목록 (해당 상품은 쿠폰 적용에서 제외) | +| included_categories | array | `[]` | 적용 대상 포함 카테고리 목록 (target_scope=categories 시 이 카테고리에만 적용) | +| excluded_categories | array | `[]` | 적용 제외 카테고리 목록 (해당 카테고리는 쿠폰 적용에서 제외) | +| issues_count | integer | `0` | issues 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "쿠폰 정보를 조회했습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 쿠폰", + "en": "API Doc Sample Coupon" + }, + "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": 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 + }, + "USD": { + "price": 0.85, + "formatted": "$0.85", + "is_default": false, + "editable": false, + "exchange_rate": 0.85 + }, + "JPY": { + "price": 115, + "formatted": "¥115", + "is_default": false, + "editable": false, + "exchange_rate": 115 + }, + "CNY": { + "price": 5.8, + "formatted": "元5.80", + "is_default": false, + "editable": false, + "exchange_rate": 5.8 + }, + "EUR": { + "price": 0.78, + "formatted": "€0.78", + "is_default": false, + "editable": false, + "exchange_rate": 0.78 + } + }, + "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": null, + "valid_to": null, + "valid_period_formatted": "-", + "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": null, + "created_by_name": "-", + "created_by_email": null, + "creator": null, + "included_products": [], + "excluded_products": [], + "included_categories": [], + "excluded_categories": [], + "issues_count": 0, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -295,10 +703,57 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.coupon.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/promotion-coupons/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "description": [ + "예시 내용입니다." + ], + "target_type": "product_amount", + "discount_type": "fixed", + "discount_value": 1, + "discount_max_amount": 1, + "min_order_amount": 1, + "issue_method": "direct", + "issue_condition": "manual", + "issue_status": "issuing", + "total_quantity": 1, + "per_user_limit": 1, + "valid_type": "period", + "valid_days": 1, + "valid_from": "2026-01-01", + "valid_to": "2026-01-01", + "issue_from": "2026-01-01", + "issue_to": "2026-01-01", + "is_combinable": true, + "target_scope": "all", + "products": [ + "예시값" + ], + "categories": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -326,10 +781,30 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | id | path | string | 예 | — | 대상 리소스의 식별자 | | user_uuids | body | array | 예 | min 1 | 쿠폰을 직접 발급할 대상 회원 UUID 배열 (내부 회원 ID 로 해석 후 발급) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/promotion-coupons/1/issue-direct HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "user_uuids": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -361,9 +836,45 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.coupon.issues_list_validation_rules`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/promotion-coupons/1/issues?user_id=9f8b2c1a-4d3e-4a2b-8c1d-0e1f2a3b4c5d&status=available&per_page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "쿠폰 발급 내역을 조회했습니다.", + "data": { + "data": [], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 0, + "from": null, + "to": null, + "has_more_pages": false + } + } +} +``` **에러 응답** @@ -392,10 +903,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | id | path | string | 예 | — | 대상 리소스의 식별자 | | issueId | path | string | 예 | — | 대상 issue의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/promotion-coupons/1/issues/{issueId} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md b/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md index 5d32222e..2d66acb9 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md @@ -29,10 +29,22 @@ | --- | --- | --- | --- | --- | --- | | hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/review-image/{hash} HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md b/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md index e5b41f8e..9b9f8036 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md @@ -44,6 +44,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.review.list_validation_rules`, `sirsoft-ecommerce.review.list_validation_messages`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/reviews?search_field=all&search_keyword=%EC%98%88%EC%8B%9C%EA%B0%92&rating=1&reply_status=all&photo=photo&has_photo=1&status=%EC%98%88%EC%8B%9C%EA%B0%92&start_date=2026-01-01&end_date=2026-01-01&sort=created_at_desc&sort_by=created_at&sort_order=asc&per_page=1&page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -80,6 +89,106 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | | abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "리뷰 목록을 조회했습니다.", + "data": { + "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" + }, + "product": { + "id": 1, + "name": "API 문서 샘플 상품", + "thumbnail_url": null + }, + "option_snapshot": null, + "option_snapshot_label": "", + "rating": 5, + "content": "Alias quas iusto dolorem eum eveniet ad omnis. Id neque consequatur fuga ut. Enim cum mollitia nisi. Adipisci sunt tenetur et tempora tempora eius rerum.", + "content_mode": "text", + "status": "visible", + "status_label": "전시중", + "status_badge_color": "blue", + "images": [], + "image_count": 0, + "orderOption": { + "id": 1, + "order_id": 5, + "order_number": "ORD-20260708-000002", + "quantity": 2, + "created_at": "2026-07-08 10:44:49" + }, + "has_reply": false, + "has_reply_label": "미답변", + "has_reply_badge_color": "gray", + "reply_content": null, + "reply_content_mode": "text", + "reply_admin_uuid": null, + "reply_admin": null, + "replied_at": null, + "reply_updated_at": null, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "abilities": { + "can_update": true, + "can_delete": true + } + } + ], + "links": { + "first": "https://api.example.com/api/modules/sirsoft-ecommerce/admin/reviews?page=1", + "last": "https://api.example.com/api/modules/sirsoft-ecommerce/admin/reviews?page=1", + "prev": null, + "next": null + }, + "meta": { + "current_page": 1, + "from": 1, + "last_page": 1, + "links": [ + { + "url": null, + "label": "pagination.previous", + "page": null, + "active": false + }, + { + "url": "https://api.example.com/api/modules/sirsoft-ecommerce/admin/reviews?page=1", + "label": "1", + "page": 1, + "active": true + }, + { + "url": null, + "label": "pagination.next", + "page": null, + "active": false + } + ], + "path": "https://api.example.com/api/modules/sirsoft-ecommerce/admin/reviews", + "per_page": 25, + "to": 1, + "total": 1 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -109,10 +218,32 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.review.bulk_validation_rules`, `sirsoft-ecommerce.review.bulk_validation_messages`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/reviews/bulk HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "action": "delete", + "status": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -138,9 +269,38 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | review | path | string | 예 | — | 대상 review의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/reviews/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| deleted | boolean | `true` | 삭제 처리 성공 여부 (true 이면 리뷰와 첨부 이미지가 제거됨) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "리뷰가 삭제되었습니다.", + "data": { + "deleted": true + } +} +``` **에러 응답** @@ -167,6 +327,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | review | path | string | 예 | — | 대상 review의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/reviews/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -203,6 +372,67 @@ _단건 응답: `data` 객체의 필드._ | updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | | abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```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" + }, + "product": { + "id": 1, + "name": "API 문서 샘플 상품", + "thumbnail_url": null + }, + "option_snapshot": null, + "option_snapshot_label": "", + "rating": 5, + "content": "Alias quas iusto dolorem eum eveniet ad omnis. Id neque consequatur fuga ut. Enim cum mollitia nisi. Adipisci sunt tenetur et tempora tempora eius rerum.", + "content_mode": "text", + "status": "visible", + "status_label": "전시중", + "status_badge_color": "blue", + "images": [], + "image_count": 0, + "orderOption": { + "id": 1, + "order_id": 5, + "order_number": "ORD-20260708-000002", + "quantity": 2, + "created_at": "2026-07-08 10:44:49" + }, + "has_reply": false, + "has_reply_label": "미답변", + "has_reply_badge_color": "gray", + "reply_content": null, + "reply_content_mode": "text", + "reply_admin_uuid": null, + "reply_admin": null, + "replied_at": null, + "reply_updated_at": null, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "abilities": { + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -228,9 +458,89 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | review | path | string | 예 | — | 대상 review의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/reviews/1/reply HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| product_id | integer | `1` | product 식별자 (연관 리소스 참조) | +| order_option_id | integer | `1` | order option 식별자 (연관 리소스 참조) | +| user_id | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | user 식별자 (연관 리소스 참조) | +| user | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 대상 사용자 정보 객체 (uuid/name/email 등 — user 관계 파생) | +| option_snapshot | null | `null` | 주문 시점 옵션 스냅샷 (옵션명 보존용) | +| option_snapshot_label | string | `` | `option_snapshot` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| rating | integer | `5` | 별점 (1~5) | +| content | string | `Alias quas iusto dolorem eum eveniet …` | 본문 내용 | +| content_mode | string | `text` | 콘텐츠 모드: text / html | +| status | string | `visible` | 상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조) | +| status_label | string | `전시중` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| status_badge_color | string | `blue` | 상태 뱃지 색상 (visible=blue / hidden=gray) | +| has_reply | boolean | `false` | reply 여부 | +| has_reply_label | string | `미답변` | `has_reply` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| has_reply_badge_color | string | `gray` | 답변 여부 뱃지 색상 (답변완료=green / 미답변=gray) | +| reply_content | null | `null` | 판매자 답변 내용 | +| reply_content_mode | string | `text` | 답변 콘텐츠 모드: text / html | +| replied_at | null | `null` | replied 일시 | +| reply_updated_at | null | `null` | reply updated 일시 | +| 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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. Enim cum mollitia nisi. Adipisci sunt tenetur et tempora tempora eius rerum.", + "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 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "abilities": { + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -261,9 +571,95 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.review.store_reply_validation_rules`, `sirsoft-ecommerce.review.store_reply_validation_messages`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/reviews/1/reply HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "reply_content": "예시 내용입니다.", + "reply_content_mode": "text" +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| product_id | integer | `1` | product 식별자 (연관 리소스 참조) | +| order_option_id | integer | `1` | order option 식별자 (연관 리소스 참조) | +| user_id | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | user 식별자 (연관 리소스 참조) | +| user | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 대상 사용자 정보 객체 (uuid/name/email 등 — user 관계 파생) | +| option_snapshot | null | `null` | 주문 시점 옵션 스냅샷 (옵션명 보존용) | +| option_snapshot_label | string | `` | `option_snapshot` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| rating | integer | `5` | 별점 (1~5) | +| content | string | `Alias quas iusto dolorem eum eveniet …` | 본문 내용 | +| content_mode | string | `text` | 콘텐츠 모드: text / html | +| status | string | `visible` | 상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조) | +| status_label | string | `전시중` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| status_badge_color | string | `blue` | 상태 뱃지 색상 (visible=blue / hidden=gray) | +| has_reply | boolean | `true` | reply 여부 | +| has_reply_label | string | `답변완료` | `has_reply` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| has_reply_badge_color | string | `green` | 답변 여부 뱃지 색상 (답변완료=green / 미답변=gray) | +| reply_content | string | `실측 예시값` | 판매자 답변 내용 | +| reply_content_mode | string | `text` | 답변 콘텐츠 모드: text / html | +| replied_at | string | `2026-07-08 15:00:32` | replied 일시 | +| reply_updated_at | null | `null` | reply updated 일시 | +| created_at | string | `2026-07-08 10:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 15:00:32` | 최종 수정 일시 | +| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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. Enim cum mollitia nisi. Adipisci sunt tenetur et tempora tempora eius rerum.", + "content_mode": "text", + "status": "visible", + "status_label": "전시중", + "status_badge_color": "blue", + "has_reply": true, + "has_reply_label": "답변완료", + "has_reply_badge_color": "green", + "reply_content": "실측 예시값", + "reply_content_mode": "text", + "replied_at": "2026-07-08 15:00:32", + "reply_updated_at": null, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:00:32", + "abilities": { + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -294,10 +690,28 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.review.update_status_validation_rules`, `sirsoft-ecommerce.review.update_status_validation_messages`). +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/reviews/1/status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "status": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -330,10 +744,32 @@ _단건 응답: `data` 객체의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.review.store_validation_rules`, `sirsoft-ecommerce.review.store_validation_messages`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/user/reviews HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "product_id": 1, + "order_option_id": 1, + "rating": 1, + "content": "예시 내용입니다.", + "content_mode": "text" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -359,9 +795,40 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | orderOptionId | path | string | 예 | — | 대상 order option의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/reviews/can-write/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| can_write | boolean | `false` | write 수행 가능 여부 (권한 기반) | +| reason | string | `not_own_order` | 리뷰 작성 불가 사유 코드 (`can_write` 가 false 일 때만 값이 있고 가능하면 null. order_option_not_found 주문옵션 없음, not_own_order 본인 주문 아님, not_confirmed 구매확정 전, deadline_passed 작성 기한 초과, already_written 이미 작성함) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "리뷰 작성 가능 여부를 확인했습니다.", + "data": { + "can_write": false, + "reason": "not_own_order" + } +} +``` **에러 응답** @@ -387,9 +854,38 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | review | path | string | 예 | — | 대상 review의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/user/reviews/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| deleted | boolean | `true` | 삭제 처리 성공 여부 (true 이면 리뷰와 첨부 이미지가 제거됨) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "리뷰가 삭제되었습니다.", + "data": { + "deleted": true + } +} +``` **에러 응답** @@ -417,10 +913,31 @@ _단건 응답: `data` 객체의 필드._ | review | path | string | 예 | — | 대상 review의 식별자 | | image | body | file | 예 | max 10240 | 첨부할 이미지 파일 (최대 용량은 리뷰 설정 `review_settings.max_image_size_mb` 기반, 폴백 10MB) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/user/reviews/1/images HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="image"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -448,10 +965,23 @@ _단건 응답: `data` 객체의 필드._ | review | path | string | 예 | — | 대상 review의 식별자 | | image | path | string | 예 | — | 대상 image의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/user/reviews/1/images/{image} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/settings.md b/modules/_bundled/sirsoft-ecommerce/docs/api/settings.md index 0b35ab25..2d8668c0 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/settings.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/settings.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/settings HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -46,6 +55,951 @@ _단건 응답: `data` 객체의 필드._ | available_pg_providers | array | `[{"id":"kginicis","name_key":"sirsoft-pay_kginicis::provi…` | 설치된 PG 플러그인이 훅으로 등록한 PG 제공자 목록 (id·name_key·지원 결제수단) | | abilities | object | `{"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-ecommerce::messages.settings.fetch_success", + "data": { + "basic_info": { + "shop_name": "", + "route_path": "shop", + "no_route": false, + "company_name": "", + "business_number": "", + "ceo_name": "", + "business_type": "", + "business_category": "", + "zipcode": "", + "base_address": "", + "detail_address": "", + "phone": "", + "fax": "", + "email": "", + "privacy_officer": "", + "privacy_officer_email": "", + "mail_order_number": "", + "telecom_number": "" + }, + "language_currency": { + "default_currency": "KRW", + "currencies": [ + { + "code": "KRW", + "name": { + "ko": "KRW (원)", + "en": "KRW (Won)", + "fr": "KRW (원)" + }, + "symbol": "₩", + "exchange_rate": null, + "base_unit": 1000, + "rounding_unit": "1", + "rounding_method": "floor", + "decimal_places": 0, + "is_default": true, + "locales": [ + "ko" + ], + "flag": "🇰🇷" + }, + { + "code": "USD", + "name": { + "ko": "USD (달러)", + "en": "USD (Dollar)", + "fr": "USD (달러)" + }, + "symbol": "$", + "exchange_rate": 0.85, + "base_unit": 1, + "rounding_unit": "0.01", + "rounding_method": "round", + "decimal_places": 2, + "is_default": false, + "locales": [ + "en" + ], + "flag": "🇺🇸" + }, + { + "code": "JPY", + "name": { + "ko": "JPY (엔)", + "en": "JPY (Yen)", + "fr": "JPY (엔)" + }, + "symbol": "¥", + "exchange_rate": 115, + "base_unit": 100, + "rounding_unit": "1", + "rounding_method": "floor", + "decimal_places": 0, + "is_default": false, + "locales": [ + "en" + ], + "flag": "🇯🇵" + }, + { + "code": "CNY", + "name": { + "ko": "CNY (위안)", + "en": "CNY (Yuan)", + "fr": "CNY (위안)" + }, + "symbol": "元", + "exchange_rate": 5.8, + "base_unit": 1, + "rounding_unit": "0.01", + "rounding_method": "round", + "decimal_places": 2, + "is_default": false, + "locales": [ + "en" + ], + "flag": "🇨🇳" + }, + { + "code": "EUR", + "name": { + "ko": "EUR (유로)", + "en": "EUR (Euro)", + "fr": "EUR (유로)" + }, + "symbol": "€", + "exchange_rate": 0.78, + "base_unit": 1, + "rounding_unit": "0.01", + "rounding_method": "round", + "decimal_places": 2, + "is_default": false, + "locales": [ + "en" + ], + "flag": "🇪🇺" + } + ] + }, + "order_settings": { + "default_pg_provider": null, + "payment_methods": [ + { + "id": "card", + "pg_provider": null, + "sort_order": 1, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "신용카드", + "en": "Credit Card", + "fr": "신용카드" + }, + "_cached_description": { + "ko": "신용카드로 안전하게 결제", + "en": "Pay securely with credit card", + "fr": "신용카드로 안전하게 결제" + }, + "_cached_icon": "credit-card", + "_cached_source": "builtin" + }, + { + "id": "vbank", + "pg_provider": null, + "sort_order": 2, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "order_placed", + "_cached_name": { + "ko": "가상계좌", + "en": "Virtual Account", + "fr": "가상계좌" + }, + "_cached_description": { + "ko": "가상계좌로 입금", + "en": "Pay via virtual account", + "fr": "가상계좌로 입금" + }, + "_cached_icon": "money-check", + "_cached_source": "builtin" + }, + { + "id": "dbank", + "pg_provider": null, + "sort_order": 3, + "is_active": true, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "order_placed", + "_cached_name": { + "ko": "무통장입금", + "en": "Bank Transfer", + "fr": "무통장입금" + }, + "_cached_description": { + "ko": "지정 계좌로 직접 입금", + "en": "Direct bank transfer", + "fr": "지정 계좌로 직접 입금" + }, + "_cached_icon": "building-columns", + "_cached_source": "builtin" + }, + { + "id": "bank", + "pg_provider": null, + "sort_order": 4, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "계좌이체", + "en": "Account Transfer", + "fr": "계좌이체" + }, + "_cached_description": { + "ko": "실시간 계좌이체", + "en": "Real-time bank transfer", + "fr": "실시간 계좌이체" + }, + "_cached_icon": "building-columns", + "_cached_source": "builtin" + }, + { + "id": "phone", + "pg_provider": null, + "sort_order": 5, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "휴대폰결제", + "en": "Mobile Payment", + "fr": "휴대폰결제" + }, + "_cached_description": { + "ko": "휴대폰 소액결제", + "en": "Mobile phone payment", + "fr": "휴대폰 소액결제" + }, + "_cached_icon": "mobile-screen-button", + "_cached_source": "builtin" + }, + { + "id": "point", + "pg_provider": null, + "sort_order": 6, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "포인트결제", + "en": "Points", + "fr": "포인트결제" + }, + "_cached_description": { + "ko": "적립 포인트로 결제", + "en": "Pay with points", + "fr": "적립 포인트로 결제" + }, + "_cached_icon": "coins", + "_cached_source": "builtin" + }, + { + "id": "deposit", + "pg_provider": null, + "sort_order": 7, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "예치금결제", + "en": "Store Credit", + "fr": "예치금결제" + }, + "_cached_description": { + "ko": "예치금으로 결제", + "en": "Pay with store credit", + "fr": "예치금으로 결제" + }, + "_cached_icon": "wallet", + "_cached_source": "builtin" + }, + { + "id": "free", + "pg_provider": null, + "sort_order": 8, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "무료", + "en": "Free", + "fr": "무료" + }, + "_cached_description": { + "ko": "결제 없이 주문 완료", + "en": "Order without payment", + "fr": "결제 없이 주문 완료" + }, + "_cached_icon": "gift", + "_cached_source": "builtin" + } + ], + "banks": [ + { + "code": "004", + "name": { + "ko": "국민은행", + "en": "Kookmin Bank" + } + }, + { + "code": "088", + "name": { + "ko": "신한은행", + "en": "Shinhan Bank" + } + }, + { + "code": "020", + "name": { + "ko": "우리은행", + "en": "Woori Bank" + } + }, + { + "code": "081", + "name": { + "ko": "하나은행", + "en": "Hana Bank" + } + }, + { + "code": "003", + "name": { + "ko": "IBK기업은행", + "en": "IBK Industrial Bank" + } + }, + { + "code": "011", + "name": { + "ko": "NH농협은행", + "en": "NH Nonghyup Bank" + } + }, + { + "code": "071", + "name": { + "ko": "우체국", + "en": "Korea Post" + } + }, + { + "code": "031", + "name": { + "ko": "DGB대구은행", + "en": "DGB Daegu Bank" + } + }, + { + "code": "032", + "name": { + "ko": "BNK부산은행", + "en": "BNK Busan Bank" + } + }, + { + "code": "034", + "name": { + "ko": "광주은행", + "en": "Kwangju Bank" + } + }, + { + "code": "035", + "name": { + "ko": "제주은행", + "en": "Jeju Bank" + } + }, + { + "code": "037", + "name": { + "ko": "전북은행", + "en": "Jeonbuk Bank" + } + }, + { + "code": "039", + "name": { + "ko": "BNK경남은행", + "en": "BNK Kyongnam Bank" + } + }, + { + "code": "045", + "name": { + "ko": "새마을금고", + "en": "MG Community Credit Cooperatives" + } + }, + { + "code": "048", + "name": { + "ko": "신협", + "en": "KFCC" + } + }, + { + "code": "090", + "name": { + "ko": "카카오뱅크", + "en": "Kakao Bank" + } + }, + { + "code": "092", + "name": { + "ko": "토스뱅크", + "en": "Toss Bank" + } + } + ], + "bank_accounts": [ + { + "bank_code": "004", + "account_number": "", + "account_holder": "", + "is_active": false, + "is_default": false + } + ], + "auto_cancel_expired": true, + "auto_cancel_days": 3, + "cart_expiry_days": 30, + "stock_restore_on_cancel": true, + "cancellable_statuses": [ + "payment_complete" + ], + "confirmable_statuses": [ + "shipping", + "delivered" + ] + }, + "shipping": { + "default_country": "KR", + "available_countries": [ + { + "code": "KR", + "name": { + "ko": "대한민국", + "en": "South Korea", + "fr": "대한민국" + }, + "is_active": true + }, + { + "code": "US", + "name": { + "ko": "미국", + "en": "United States", + "fr": "미국" + }, + "is_active": false + }, + { + "code": "JP", + "name": { + "ko": "일본", + "en": "Japan", + "fr": "일본" + }, + "is_active": false + }, + { + "code": "CN", + "name": { + "ko": "중국", + "en": "China", + "fr": "중국" + }, + "is_active": false + }, + { + "code": "SG", + "name": { + "ko": "싱가포르", + "en": "Singapore", + "fr": "싱가포르" + }, + "is_active": false + }, + { + "code": "HK", + "name": { + "ko": "홍콩", + "en": "Hong Kong", + "fr": "홍콩" + }, + "is_active": false + }, + { + "code": "TW", + "name": { + "ko": "대만", + "en": "Taiwan", + "fr": "대만" + }, + "is_active": false + }, + { + "code": "VN", + "name": { + "ko": "베트남", + "en": "Vietnam", + "fr": "베트남" + }, + "is_active": false + }, + { + "code": "TH", + "name": { + "ko": "태국", + "en": "Thailand", + "fr": "태국" + }, + "is_active": false + }, + { + "code": "MY", + "name": { + "ko": "말레이시아", + "en": "Malaysia", + "fr": "말레이시아" + }, + "is_active": false + } + ], + "international_shipping_enabled": false, + "free_shipping_threshold": 50000, + "free_shipping_enabled": true, + "address_validation_enabled": false, + "address_api_provider": "kakao", + "carriers": [ + { + "id": 13, + "code": "apidoc", + "name": { + "ko": "API 문서 샘플 배송사", + "en": "API Doc Sample Carrier" + }, + "type": "domestic", + "tracking_url": null, + "is_active": true, + "sort_order": 0 + }, + { + "id": 1, + "code": "cj", + "name": { + "ko": "CJ대한통운", + "en": "CJ Logistics" + }, + "type": "domestic", + "tracking_url": "https://trace.cjlogistics.com/next/tracking.html?wblNo={tracking_number}", + "is_active": true, + "sort_order": 1 + }, + { + "id": 2, + "code": "hanjin", + "name": { + "ko": "한진택배", + "en": "Hanjin Express" + }, + "type": "domestic", + "tracking_url": "https://www.hanjin.com/kor/CMS/DeliveryMgr/WaybillResult.do?wblnb={tracking_number}", + "is_active": true, + "sort_order": 2 + }, + { + "id": 3, + "code": "lotte", + "name": { + "ko": "롯데택배", + "en": "Lotte Global Logistics" + }, + "type": "domestic", + "tracking_url": "https://www.lotteglogis.com/home/reservation/tracking/link498?InvNo={tracking_number}", + "is_active": true, + "sort_order": 3 + }, + { + "id": 4, + "code": "logen", + "name": { + "ko": "로젠택배", + "en": "Logen Logistics" + }, + "type": "domestic", + "tracking_url": "https://www.ilogen.com/web/personal/trace/{tracking_number}", + "is_active": true, + "sort_order": 4 + }, + { + "id": 5, + "code": "ups", + "name": { + "ko": "UPS", + "en": "UPS" + }, + "type": "international", + "tracking_url": "https://www.ups.com/track?tracknum={tracking_number}", + "is_active": true, + "sort_order": 5 + }, + { + "id": 6, + "code": "ems", + "name": { + "ko": "EMS", + "en": "EMS" + }, + "type": "international", + "tracking_url": "https://service.epost.go.kr/trace.RetrieveEmsRi498.postal?POST_CODE={tracking_number}", + "is_active": true, + "sort_order": 6 + }, + { + "id": 7, + "code": "dhl", + "name": { + "ko": "DHL", + "en": "DHL" + }, + "type": "international", + "tracking_url": "https://www.dhl.com/kr-ko/home/tracking/tracking-express.html?submit=1&tracking-id={tracking_number}", + "is_active": true, + "sort_order": 7 + }, + { + "id": 8, + "code": "fedex", + "name": { + "ko": "FedEx", + "en": "FedEx" + }, + "type": "international", + "tracking_url": "https://www.fedex.com/fedextrack/?tracknumbers={tracking_number}", + "is_active": true, + "sort_order": 8 + }, + { + "id": 9, + "code": "sf", + "name": { + "ko": "SF Express", + "en": "SF Express" + }, + "type": "international", + "tracking_url": null, + "is_active": true, + "sort_order": 9 + }, + { + "id": 10, + "code": "yamato", + "name": { + "ko": "야마토운수", + "en": "Yamato Transport" + }, + "type": "international", + "tracking_url": null, + "is_active": true, + "sort_order": 10 + }, + { + "id": 11, + "code": "sagawa", + "name": { + "ko": "사가와익스프레스", + "en": "Sagawa Express" + }, + "type": "international", + "tracking_url": null, + "is_active": true, + "sort_order": 11 + }, + { + "id": 12, + "code": "other", + "name": { + "ko": "기타", + "en": "Other" + }, + "type": "domestic", + "tracking_url": null, + "is_active": true, + "sort_order": 99 + } + ], + "types": [], + "api_request_fields": [ + { + "value": "policy_id", + "label": "배송정책 ID" + }, + { + "value": "country_code", + "label": "국가 코드" + }, + { + "value": "items", + "label": "주문 항목" + }, + { + "value": "group_total", + "label": "그룹 합계 금액" + }, + { + "value": "total_quantity", + "label": "총 수량" + } + ], + "api_http_methods": [ + { + "value": "GET", + "label": "GET" + }, + { + "value": "POST", + "label": "POST" + } + ], + "api_auth_types": [ + { + "value": "none", + "label": "인증 없음" + }, + { + "value": "bearer", + "label": "Bearer 토큰" + }, + { + "value": "custom_header", + "label": "커스텀 헤더" + } + ], + "api_response_types": [ + { + "value": "json", + "label": "JSON" + }, + { + "value": "text", + "label": "텍스트" + } + ] + }, + "seo": { + "meta_category_title": "{commerce_name} - {category_name}", + "meta_category_description": "", + "meta_search_title": "{commerce_name} - {keyword_name}", + "meta_search_description": "", + "meta_product_title": "{commerce_name} - {product_name}", + "meta_product_description": "", + "meta_shop_index_title": "{commerce_name}", + "meta_shop_index_description": "", + "seo_category": true, + "seo_search_result": true, + "seo_product_detail": true, + "seo_shop_index": true + }, + "review_settings": { + "write_deadline_days": 90, + "max_images": 5, + "max_image_size_mb": 10 + }, + "inquiry": { + "board_slug": null + }, + "notifications": { + "channels": [ + { + "id": "mail", + "is_active": true, + "sort_order": 1 + }, + { + "id": "database", + "is_active": true, + "sort_order": 2 + } + ] + }, + "mileage": { + "enabled": false, + "default_earn_rate": 1, + "earn_trigger": "confirmed", + "earn_delay_days": "0", + "currency_rules": [ + { + "currency_code": "KRW", + "point_value": 1, + "min_use_amount": 1000, + "use_unit": 10, + "max_use_type": "fixed", + "max_use_percent": 30, + "max_use_value": 50000 + } + ], + "expiry_enabled": true, + "expiry_days": 365, + "expiry_notification_enabled": true, + "expiry_notification_days_before": 7, + "notification_channels": [ + "mail", + "database" + ] + }, + "claim": { + "refund_reasons": [ + { + "id": 1, + "type": "refund", + "code": "order_mistake", + "name": { + "ko": "주문 실수", + "en": "Order Mistake" + }, + "localized_name": "주문 실수", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 0 + }, + { + "id": 8, + "type": "refund", + "code": "apidoc_sample", + "name": { + "ko": "API 문서 샘플 사유", + "en": "API Doc Sample Reason" + }, + "localized_name": "API 문서 샘플 사유", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 0 + }, + { + "id": 2, + "type": "refund", + "code": "changed_mind", + "name": { + "ko": "단순 변심", + "en": "Changed Mind" + }, + "localized_name": "단순 변심", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 1 + }, + { + "id": 3, + "type": "refund", + "code": "reorder_other", + "name": { + "ko": "다른 상품으로 재주문", + "en": "Reorder with Different Product" + }, + "localized_name": "다른 상품으로 재주문", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 2 + }, + { + "id": 4, + "type": "refund", + "code": "delayed_delivery", + "name": { + "ko": "배송 지연", + "en": "Delayed Delivery" + }, + "localized_name": "배송 지연", + "fault_type": "seller", + "is_user_selectable": true, + "is_active": true, + "sort_order": 3 + }, + { + "id": 5, + "type": "refund", + "code": "product_info_different", + "name": { + "ko": "상품 정보 상이", + "en": "Product Info Different" + }, + "localized_name": "상품 정보 상이", + "fault_type": "seller", + "is_user_selectable": true, + "is_active": true, + "sort_order": 4 + }, + { + "id": 6, + "type": "refund", + "code": "admin_cancel", + "name": { + "ko": "관리자 취소", + "en": "Admin Cancel" + }, + "localized_name": "관리자 취소", + "fault_type": "seller", + "is_user_selectable": false, + "is_active": true, + "sort_order": 5 + }, + { + "id": 7, + "type": "refund", + "code": "etc", + "name": { + "ko": "기타", + "en": "Etc" + }, + "localized_name": "기타", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 6 + } + ] + }, + "available_pg_providers": [], + "abilities": { + "can_update": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -80,9 +1034,1009 @@ _단건 응답: `data` 객체의 필드._ | mileage | body | array | 아니오 | — | 마일리지 설정 섹션 (사용 여부·기본 적립률·적립 트리거·통화별 규칙·소멸/소멸 알림) | | shipping | body | array | 아니오 | — | 배송 설정 섹션 (기본 국가·배송 가능 국가·무료배송·배송사(carriers)·배송유형(types) — carriers/types는 DB 동기화 대상으로 분리 저장) | +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/settings HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "_tab": "basic_info", + "notifications": [ + "예시값" + ], + "basic_info": [ + "예시값" + ], + "language_currency": [ + "예시값" + ], + "seo": [ + "예시값" + ], + "inquiry": [ + "예시값" + ], + "order_settings": [ + "예시값" + ], + "claim": [ + "예시값" + ], + "review_settings": [ + "예시값" + ], + "mileage": [ + "예시값" + ], + "shipping": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| basic_info | object | `{"shop_name":"","route_path":"shop","no_route":false,"com…` | 쇼핑몰 기본 정보 (쇼핑몰명·라우트 경로·상호·사업자번호·주소·연락처·이메일 등) | +| language_currency | object | `{"default_currency":"KRW","currencies":[{"code":"KRW","na…` | 통화 설정 (기본 통화 + 등록 통화 목록: 코드·다국어명·환율·기호·국기·반올림 규칙) | +| order_settings | object | `{"default_pg_provider":null,"payment_methods":[{"id":"car…` | 주문/결제 설정 (기본 PG·병합된 결제수단·은행/무통장 계좌·자동취소·장바구니 만료 등) | +| shipping | object | `{"default_country":"KR","available_countries":[{"code":"K…` | 배송 설정 (기본 국가·배송 가능 국가·무료배송·DB 관리 배송사(carriers)·배송유형(types)·계산 API 후보 필드 포함) | +| seo | object | `{"meta_category_title":"{commerce_name} - {category_name}…` | SEO 메타 설정 (카테고리·검색·상품·쇼핑몰 인덱스별 메타 타이틀/설명 및 SEO 활성 토글) | +| review_settings | object | `{"write_deadline_days":90,"max_images":5,"max_image_size_…` | 리뷰 정책 (작성 기한일·이미지 최대 개수·이미지 최대 용량 MB) | +| inquiry | object | `{"board_slug":null}` | 문의 연동 설정 (문의 게시판 slug) | +| notifications | object | `{"channels":[{"id":"mail","is_active":true,"sort_order":1…` | 알림 채널 설정 (채널 ID·활성 여부·정렬 순서) | +| mileage | object | `{"enabled":false,"default_earn_rate":1,"earn_trigger":"co…` | 마일리지 설정 (사용 여부·기본 적립률·적립 트리거·통화별 규칙·소멸/소멸 알림·실제 활성 알림 채널 포함) | +| claim | object | `{"refund_reasons":[{"id":1,"type":"refund","code":"order_…` | 클레임 설정 (DB 관리 대상인 환불 사유 목록: 코드·다국어명·귀책 유형·노출/활성 여부) | +| available_pg_providers | array | `[]` | 설치된 PG 플러그인이 훅으로 등록한 PG 제공자 목록 (id·name_key·지원 결제수단) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-ecommerce::messages.settings.save_success", + "data": { + "basic_info": { + "shop_name": "", + "route_path": "shop", + "no_route": false, + "company_name": "", + "business_number": "", + "ceo_name": "", + "business_type": "", + "business_category": "", + "zipcode": "", + "base_address": "", + "detail_address": "", + "phone": "", + "fax": "", + "email": "", + "privacy_officer": "", + "privacy_officer_email": "", + "mail_order_number": "", + "telecom_number": "" + }, + "language_currency": { + "default_currency": "KRW", + "currencies": [ + { + "code": "KRW", + "name": { + "ko": "KRW (원)", + "en": "KRW (Won)", + "fr": "KRW (원)" + }, + "symbol": "₩", + "exchange_rate": null, + "base_unit": 1000, + "rounding_unit": "1", + "rounding_method": "floor", + "decimal_places": 0, + "is_default": true, + "locales": [ + "ko" + ], + "flag": "🇰🇷" + }, + { + "code": "USD", + "name": { + "ko": "USD (달러)", + "en": "USD (Dollar)", + "fr": "USD (달러)" + }, + "symbol": "$", + "exchange_rate": 0.85, + "base_unit": 1, + "rounding_unit": "0.01", + "rounding_method": "round", + "decimal_places": 2, + "is_default": false, + "locales": [ + "en" + ], + "flag": "🇺🇸" + }, + { + "code": "JPY", + "name": { + "ko": "JPY (엔)", + "en": "JPY (Yen)", + "fr": "JPY (엔)" + }, + "symbol": "¥", + "exchange_rate": 115, + "base_unit": 100, + "rounding_unit": "1", + "rounding_method": "floor", + "decimal_places": 0, + "is_default": false, + "locales": [ + "en" + ], + "flag": "🇯🇵" + }, + { + "code": "CNY", + "name": { + "ko": "CNY (위안)", + "en": "CNY (Yuan)", + "fr": "CNY (위안)" + }, + "symbol": "元", + "exchange_rate": 5.8, + "base_unit": 1, + "rounding_unit": "0.01", + "rounding_method": "round", + "decimal_places": 2, + "is_default": false, + "locales": [ + "en" + ], + "flag": "🇨🇳" + }, + { + "code": "EUR", + "name": { + "ko": "EUR (유로)", + "en": "EUR (Euro)", + "fr": "EUR (유로)" + }, + "symbol": "€", + "exchange_rate": 0.78, + "base_unit": 1, + "rounding_unit": "0.01", + "rounding_method": "round", + "decimal_places": 2, + "is_default": false, + "locales": [ + "en" + ], + "flag": "🇪🇺" + } + ] + }, + "order_settings": { + "default_pg_provider": null, + "payment_methods": [ + { + "id": "card", + "pg_provider": null, + "sort_order": 1, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "신용카드", + "en": "Credit Card", + "fr": "신용카드" + }, + "_cached_description": { + "ko": "신용카드로 안전하게 결제", + "en": "Pay securely with credit card", + "fr": "신용카드로 안전하게 결제" + }, + "_cached_icon": "credit-card", + "_cached_source": "builtin" + }, + { + "id": "vbank", + "pg_provider": null, + "sort_order": 2, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "order_placed", + "_cached_name": { + "ko": "가상계좌", + "en": "Virtual Account", + "fr": "가상계좌" + }, + "_cached_description": { + "ko": "가상계좌로 입금", + "en": "Pay via virtual account", + "fr": "가상계좌로 입금" + }, + "_cached_icon": "money-check", + "_cached_source": "builtin" + }, + { + "id": "dbank", + "pg_provider": null, + "sort_order": 3, + "is_active": true, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "order_placed", + "_cached_name": { + "ko": "무통장입금", + "en": "Bank Transfer", + "fr": "무통장입금" + }, + "_cached_description": { + "ko": "지정 계좌로 직접 입금", + "en": "Direct bank transfer", + "fr": "지정 계좌로 직접 입금" + }, + "_cached_icon": "building-columns", + "_cached_source": "builtin" + }, + { + "id": "bank", + "pg_provider": null, + "sort_order": 4, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "계좌이체", + "en": "Account Transfer", + "fr": "계좌이체" + }, + "_cached_description": { + "ko": "실시간 계좌이체", + "en": "Real-time bank transfer", + "fr": "실시간 계좌이체" + }, + "_cached_icon": "building-columns", + "_cached_source": "builtin" + }, + { + "id": "phone", + "pg_provider": null, + "sort_order": 5, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "휴대폰결제", + "en": "Mobile Payment", + "fr": "휴대폰결제" + }, + "_cached_description": { + "ko": "휴대폰 소액결제", + "en": "Mobile phone payment", + "fr": "휴대폰 소액결제" + }, + "_cached_icon": "mobile-screen-button", + "_cached_source": "builtin" + }, + { + "id": "point", + "pg_provider": null, + "sort_order": 6, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "포인트결제", + "en": "Points", + "fr": "포인트결제" + }, + "_cached_description": { + "ko": "적립 포인트로 결제", + "en": "Pay with points", + "fr": "적립 포인트로 결제" + }, + "_cached_icon": "coins", + "_cached_source": "builtin" + }, + { + "id": "deposit", + "pg_provider": null, + "sort_order": 7, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "예치금결제", + "en": "Store Credit", + "fr": "예치금결제" + }, + "_cached_description": { + "ko": "예치금으로 결제", + "en": "Pay with store credit", + "fr": "예치금으로 결제" + }, + "_cached_icon": "wallet", + "_cached_source": "builtin" + }, + { + "id": "free", + "pg_provider": null, + "sort_order": 8, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "무료", + "en": "Free", + "fr": "무료" + }, + "_cached_description": { + "ko": "결제 없이 주문 완료", + "en": "Order without payment", + "fr": "결제 없이 주문 완료" + }, + "_cached_icon": "gift", + "_cached_source": "builtin" + } + ], + "banks": [ + { + "code": "004", + "name": { + "ko": "국민은행", + "en": "Kookmin Bank" + } + }, + { + "code": "088", + "name": { + "ko": "신한은행", + "en": "Shinhan Bank" + } + }, + { + "code": "020", + "name": { + "ko": "우리은행", + "en": "Woori Bank" + } + }, + { + "code": "081", + "name": { + "ko": "하나은행", + "en": "Hana Bank" + } + }, + { + "code": "003", + "name": { + "ko": "IBK기업은행", + "en": "IBK Industrial Bank" + } + }, + { + "code": "011", + "name": { + "ko": "NH농협은행", + "en": "NH Nonghyup Bank" + } + }, + { + "code": "071", + "name": { + "ko": "우체국", + "en": "Korea Post" + } + }, + { + "code": "031", + "name": { + "ko": "DGB대구은행", + "en": "DGB Daegu Bank" + } + }, + { + "code": "032", + "name": { + "ko": "BNK부산은행", + "en": "BNK Busan Bank" + } + }, + { + "code": "034", + "name": { + "ko": "광주은행", + "en": "Kwangju Bank" + } + }, + { + "code": "035", + "name": { + "ko": "제주은행", + "en": "Jeju Bank" + } + }, + { + "code": "037", + "name": { + "ko": "전북은행", + "en": "Jeonbuk Bank" + } + }, + { + "code": "039", + "name": { + "ko": "BNK경남은행", + "en": "BNK Kyongnam Bank" + } + }, + { + "code": "045", + "name": { + "ko": "새마을금고", + "en": "MG Community Credit Cooperatives" + } + }, + { + "code": "048", + "name": { + "ko": "신협", + "en": "KFCC" + } + }, + { + "code": "090", + "name": { + "ko": "카카오뱅크", + "en": "Kakao Bank" + } + }, + { + "code": "092", + "name": { + "ko": "토스뱅크", + "en": "Toss Bank" + } + } + ], + "bank_accounts": [ + { + "bank_code": "004", + "account_number": "", + "account_holder": "", + "is_active": false, + "is_default": false + } + ], + "auto_cancel_expired": true, + "auto_cancel_days": 3, + "cart_expiry_days": 30, + "stock_restore_on_cancel": true, + "cancellable_statuses": [ + "payment_complete" + ], + "confirmable_statuses": [ + "shipping", + "delivered" + ] + }, + "shipping": { + "default_country": "KR", + "available_countries": [ + { + "code": "KR", + "name": { + "ko": "대한민국", + "en": "South Korea", + "fr": "대한민국" + }, + "is_active": true + }, + { + "code": "US", + "name": { + "ko": "미국", + "en": "United States", + "fr": "미국" + }, + "is_active": false + }, + { + "code": "JP", + "name": { + "ko": "일본", + "en": "Japan", + "fr": "일본" + }, + "is_active": false + }, + { + "code": "CN", + "name": { + "ko": "중국", + "en": "China", + "fr": "중국" + }, + "is_active": false + }, + { + "code": "SG", + "name": { + "ko": "싱가포르", + "en": "Singapore", + "fr": "싱가포르" + }, + "is_active": false + }, + { + "code": "HK", + "name": { + "ko": "홍콩", + "en": "Hong Kong", + "fr": "홍콩" + }, + "is_active": false + }, + { + "code": "TW", + "name": { + "ko": "대만", + "en": "Taiwan", + "fr": "대만" + }, + "is_active": false + }, + { + "code": "VN", + "name": { + "ko": "베트남", + "en": "Vietnam", + "fr": "베트남" + }, + "is_active": false + }, + { + "code": "TH", + "name": { + "ko": "태국", + "en": "Thailand", + "fr": "태국" + }, + "is_active": false + }, + { + "code": "MY", + "name": { + "ko": "말레이시아", + "en": "Malaysia", + "fr": "말레이시아" + }, + "is_active": false + } + ], + "international_shipping_enabled": false, + "free_shipping_threshold": 50000, + "free_shipping_enabled": true, + "address_validation_enabled": false, + "address_api_provider": "kakao", + "carriers": [ + { + "id": 13, + "code": "apidoc", + "name": { + "ko": "API 문서 샘플 배송사", + "en": "API Doc Sample Carrier" + }, + "type": "domestic", + "tracking_url": null, + "is_active": true, + "sort_order": 0 + }, + { + "id": 1, + "code": "cj", + "name": { + "ko": "CJ대한통운", + "en": "CJ Logistics" + }, + "type": "domestic", + "tracking_url": "https://trace.cjlogistics.com/next/tracking.html?wblNo={tracking_number}", + "is_active": true, + "sort_order": 1 + }, + { + "id": 2, + "code": "hanjin", + "name": { + "ko": "한진택배", + "en": "Hanjin Express" + }, + "type": "domestic", + "tracking_url": "https://www.hanjin.com/kor/CMS/DeliveryMgr/WaybillResult.do?wblnb={tracking_number}", + "is_active": true, + "sort_order": 2 + }, + { + "id": 3, + "code": "lotte", + "name": { + "ko": "롯데택배", + "en": "Lotte Global Logistics" + }, + "type": "domestic", + "tracking_url": "https://www.lotteglogis.com/home/reservation/tracking/link498?InvNo={tracking_number}", + "is_active": true, + "sort_order": 3 + }, + { + "id": 4, + "code": "logen", + "name": { + "ko": "로젠택배", + "en": "Logen Logistics" + }, + "type": "domestic", + "tracking_url": "https://www.ilogen.com/web/personal/trace/{tracking_number}", + "is_active": true, + "sort_order": 4 + }, + { + "id": 5, + "code": "ups", + "name": { + "ko": "UPS", + "en": "UPS" + }, + "type": "international", + "tracking_url": "https://www.ups.com/track?tracknum={tracking_number}", + "is_active": true, + "sort_order": 5 + }, + { + "id": 6, + "code": "ems", + "name": { + "ko": "EMS", + "en": "EMS" + }, + "type": "international", + "tracking_url": "https://service.epost.go.kr/trace.RetrieveEmsRi498.postal?POST_CODE={tracking_number}", + "is_active": true, + "sort_order": 6 + }, + { + "id": 7, + "code": "dhl", + "name": { + "ko": "DHL", + "en": "DHL" + }, + "type": "international", + "tracking_url": "https://www.dhl.com/kr-ko/home/tracking/tracking-express.html?submit=1&tracking-id={tracking_number}", + "is_active": true, + "sort_order": 7 + }, + { + "id": 8, + "code": "fedex", + "name": { + "ko": "FedEx", + "en": "FedEx" + }, + "type": "international", + "tracking_url": "https://www.fedex.com/fedextrack/?tracknumbers={tracking_number}", + "is_active": true, + "sort_order": 8 + }, + { + "id": 9, + "code": "sf", + "name": { + "ko": "SF Express", + "en": "SF Express" + }, + "type": "international", + "tracking_url": null, + "is_active": true, + "sort_order": 9 + }, + { + "id": 10, + "code": "yamato", + "name": { + "ko": "야마토운수", + "en": "Yamato Transport" + }, + "type": "international", + "tracking_url": null, + "is_active": true, + "sort_order": 10 + }, + { + "id": 11, + "code": "sagawa", + "name": { + "ko": "사가와익스프레스", + "en": "Sagawa Express" + }, + "type": "international", + "tracking_url": null, + "is_active": true, + "sort_order": 11 + }, + { + "id": 12, + "code": "other", + "name": { + "ko": "기타", + "en": "Other" + }, + "type": "domestic", + "tracking_url": null, + "is_active": true, + "sort_order": 99 + } + ], + "types": [], + "api_request_fields": [ + { + "value": "policy_id", + "label": "배송정책 ID" + }, + { + "value": "country_code", + "label": "국가 코드" + }, + { + "value": "items", + "label": "주문 항목" + }, + { + "value": "group_total", + "label": "그룹 합계 금액" + }, + { + "value": "total_quantity", + "label": "총 수량" + } + ], + "api_http_methods": [ + { + "value": "GET", + "label": "GET" + }, + { + "value": "POST", + "label": "POST" + } + ], + "api_auth_types": [ + { + "value": "none", + "label": "인증 없음" + }, + { + "value": "bearer", + "label": "Bearer 토큰" + }, + { + "value": "custom_header", + "label": "커스텀 헤더" + } + ], + "api_response_types": [ + { + "value": "json", + "label": "JSON" + }, + { + "value": "text", + "label": "텍스트" + } + ] + }, + "seo": { + "meta_category_title": "{commerce_name} - {category_name}", + "meta_category_description": "", + "meta_search_title": "{commerce_name} - {keyword_name}", + "meta_search_description": "", + "meta_product_title": "{commerce_name} - {product_name}", + "meta_product_description": "", + "meta_shop_index_title": "{commerce_name}", + "meta_shop_index_description": "", + "seo_category": true, + "seo_search_result": true, + "seo_product_detail": true, + "seo_shop_index": true + }, + "review_settings": { + "write_deadline_days": 90, + "max_images": 5, + "max_image_size_mb": 10 + }, + "inquiry": { + "board_slug": null + }, + "notifications": { + "channels": [ + { + "id": "mail", + "is_active": true, + "sort_order": 1 + }, + { + "id": "database", + "is_active": true, + "sort_order": 2 + } + ] + }, + "mileage": { + "enabled": false, + "default_earn_rate": 1, + "earn_trigger": "confirmed", + "earn_delay_days": "0", + "currency_rules": [ + { + "currency_code": "KRW", + "point_value": 1, + "min_use_amount": 1000, + "use_unit": 10, + "max_use_type": "fixed", + "max_use_percent": 30, + "max_use_value": 50000 + } + ], + "expiry_enabled": true, + "expiry_days": 365, + "expiry_notification_enabled": true, + "expiry_notification_days_before": 7, + "notification_channels": [ + "mail", + "database" + ] + }, + "claim": { + "refund_reasons": [ + { + "id": 1, + "type": "refund", + "code": "order_mistake", + "name": { + "ko": "주문 실수", + "en": "Order Mistake" + }, + "localized_name": "주문 실수", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 0 + }, + { + "id": 8, + "type": "refund", + "code": "apidoc_sample", + "name": { + "ko": "API 문서 샘플 사유", + "en": "API Doc Sample Reason" + }, + "localized_name": "API 문서 샘플 사유", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 0 + }, + { + "id": 2, + "type": "refund", + "code": "changed_mind", + "name": { + "ko": "단순 변심", + "en": "Changed Mind" + }, + "localized_name": "단순 변심", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 1 + }, + { + "id": 3, + "type": "refund", + "code": "reorder_other", + "name": { + "ko": "다른 상품으로 재주문", + "en": "Reorder with Different Product" + }, + "localized_name": "다른 상품으로 재주문", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 2 + }, + { + "id": 4, + "type": "refund", + "code": "delayed_delivery", + "name": { + "ko": "배송 지연", + "en": "Delayed Delivery" + }, + "localized_name": "배송 지연", + "fault_type": "seller", + "is_user_selectable": true, + "is_active": true, + "sort_order": 3 + }, + { + "id": 5, + "type": "refund", + "code": "product_info_different", + "name": { + "ko": "상품 정보 상이", + "en": "Product Info Different" + }, + "localized_name": "상품 정보 상이", + "fault_type": "seller", + "is_user_selectable": true, + "is_active": true, + "sort_order": 4 + }, + { + "id": 6, + "type": "refund", + "code": "admin_cancel", + "name": { + "ko": "관리자 취소", + "en": "Admin Cancel" + }, + "localized_name": "관리자 취소", + "fault_type": "seller", + "is_user_selectable": false, + "is_active": true, + "sort_order": 5 + }, + { + "id": 7, + "type": "refund", + "code": "etc", + "name": { + "ko": "기타", + "en": "Etc" + }, + "localized_name": "기타", + "fault_type": "customer", + "is_user_selectable": true, + "is_active": true, + "sort_order": 6 + } + ] + }, + "available_pg_providers": [] + } +} +``` **에러 응답** @@ -109,9 +2063,523 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | banks | body | array | 아니오 | — | 무통장입금용 은행 목록 (은행 코드·다국어 은행명) | +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/settings/banks HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "banks": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| basic_info | object | `{"shop_name":"","route_path":"shop","no_route":false,"com…` | 쇼핑몰 기본 정보 (쇼핑몰명·라우트 경로·상호·사업자번호·주소·연락처·이메일 등) | +| language_currency | object | `{"default_currency":"KRW","currencies":[{"code":"KRW","na…` | 통화 설정 (기본 통화 + 등록 통화 목록: 코드·다국어명·환율·기호·국기·반올림 규칙) | +| order_settings | object | `{"default_pg_provider":null,"payment_methods":[{"id":"car…` | 주문/결제 설정 (기본 PG·병합된 결제수단·은행/무통장 계좌·자동취소·장바구니 만료 등) | +| shipping | object | `{"default_country":"KR","available_countries":[{"code":"K…` | 배송 설정 (기본 국가·배송 가능 국가·무료배송·DB 관리 배송사(carriers)·배송유형(types)·계산 API 후보 필드 포함) | +| seo | object | `{"meta_category_title":"{commerce_name} - {category_name}…` | SEO 메타 설정 (카테고리·검색·상품·쇼핑몰 인덱스별 메타 타이틀/설명 및 SEO 활성 토글) | +| review_settings | object | `{"write_deadline_days":90,"max_images":5,"max_image_size_…` | 리뷰 정책 (작성 기한일·이미지 최대 개수·이미지 최대 용량 MB) | +| inquiry | object | `{"board_slug":null}` | 문의 연동 설정 (문의 게시판 slug) | +| notifications | object | `{"channels":[{"id":"mail","is_active":true,"sort_order":1…` | 알림 채널 설정 (채널 ID·활성 여부·정렬 순서) | +| mileage | object | `{"enabled":false,"default_earn_rate":1,"earn_trigger":"co…` | 마일리지 설정 (사용 여부·기본 적립률·적립 트리거·통화별 규칙·소멸/소멸 알림·실제 활성 알림 채널 포함) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-ecommerce::messages.settings.save_success", + "data": { + "basic_info": { + "shop_name": "", + "route_path": "shop", + "no_route": false, + "company_name": "", + "business_number": "", + "ceo_name": "", + "business_type": "", + "business_category": "", + "zipcode": "", + "base_address": "", + "detail_address": "", + "phone": "", + "fax": "", + "email": "", + "privacy_officer": "", + "privacy_officer_email": "", + "mail_order_number": "", + "telecom_number": "" + }, + "language_currency": { + "default_currency": "KRW", + "currencies": [ + { + "code": "KRW", + "name": { + "ko": "KRW (원)", + "en": "KRW (Won)", + "fr": "KRW (원)" + }, + "symbol": "₩", + "exchange_rate": null, + "base_unit": 1000, + "rounding_unit": "1", + "rounding_method": "floor", + "decimal_places": 0, + "is_default": true, + "locales": [ + "ko" + ], + "flag": "🇰🇷" + }, + { + "code": "USD", + "name": { + "ko": "USD (달러)", + "en": "USD (Dollar)", + "fr": "USD (달러)" + }, + "symbol": "$", + "exchange_rate": 0.85, + "base_unit": 1, + "rounding_unit": "0.01", + "rounding_method": "round", + "decimal_places": 2, + "is_default": false, + "locales": [ + "en" + ], + "flag": "🇺🇸" + }, + { + "code": "JPY", + "name": { + "ko": "JPY (엔)", + "en": "JPY (Yen)", + "fr": "JPY (엔)" + }, + "symbol": "¥", + "exchange_rate": 115, + "base_unit": 100, + "rounding_unit": "1", + "rounding_method": "floor", + "decimal_places": 0, + "is_default": false, + "locales": [ + "en" + ], + "flag": "🇯🇵" + }, + { + "code": "CNY", + "name": { + "ko": "CNY (위안)", + "en": "CNY (Yuan)", + "fr": "CNY (위안)" + }, + "symbol": "元", + "exchange_rate": 5.8, + "base_unit": 1, + "rounding_unit": "0.01", + "rounding_method": "round", + "decimal_places": 2, + "is_default": false, + "locales": [ + "en" + ], + "flag": "🇨🇳" + }, + { + "code": "EUR", + "name": { + "ko": "EUR (유로)", + "en": "EUR (Euro)", + "fr": "EUR (유로)" + }, + "symbol": "€", + "exchange_rate": 0.78, + "base_unit": 1, + "rounding_unit": "0.01", + "rounding_method": "round", + "decimal_places": 2, + "is_default": false, + "locales": [ + "en" + ], + "flag": "🇪🇺" + } + ] + }, + "order_settings": { + "default_pg_provider": null, + "payment_methods": [ + { + "id": "card", + "pg_provider": null, + "sort_order": 1, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "신용카드", + "en": "Credit Card", + "fr": "신용카드" + }, + "_cached_description": { + "ko": "신용카드로 안전하게 결제", + "en": "Pay securely with credit card", + "fr": "신용카드로 안전하게 결제" + }, + "_cached_icon": "credit-card", + "_cached_source": "builtin" + }, + { + "id": "vbank", + "pg_provider": null, + "sort_order": 2, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "order_placed", + "_cached_name": { + "ko": "가상계좌", + "en": "Virtual Account", + "fr": "가상계좌" + }, + "_cached_description": { + "ko": "가상계좌로 입금", + "en": "Pay via virtual account", + "fr": "가상계좌로 입금" + }, + "_cached_icon": "money-check", + "_cached_source": "builtin" + }, + { + "id": "dbank", + "pg_provider": null, + "sort_order": 3, + "is_active": true, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "order_placed", + "_cached_name": { + "ko": "무통장입금", + "en": "Bank Transfer", + "fr": "무통장입금" + }, + "_cached_description": { + "ko": "지정 계좌로 직접 입금", + "en": "Direct bank transfer", + "fr": "지정 계좌로 직접 입금" + }, + "_cached_icon": "building-columns", + "_cached_source": "builtin" + }, + { + "id": "bank", + "pg_provider": null, + "sort_order": 4, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "계좌이체", + "en": "Account Transfer", + "fr": "계좌이체" + }, + "_cached_description": { + "ko": "실시간 계좌이체", + "en": "Real-time bank transfer", + "fr": "실시간 계좌이체" + }, + "_cached_icon": "building-columns", + "_cached_source": "builtin" + }, + { + "id": "phone", + "pg_provider": null, + "sort_order": 5, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "휴대폰결제", + "en": "Mobile Payment", + "fr": "휴대폰결제" + }, + "_cached_description": { + "ko": "휴대폰 소액결제", + "en": "Mobile phone payment", + "fr": "휴대폰 소액결제" + }, + "_cached_icon": "mobile-screen-button", + "_cached_source": "builtin" + }, + { + "id": "point", + "pg_provider": null, + "sort_order": 6, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "포인트결제", + "en": "Points", + "fr": "포인트결제" + }, + "_cached_description": { + "ko": "적립 포인트로 결제", + "en": "Pay with points", + "fr": "적립 포인트로 결제" + }, + "_cached_icon": "coins", + "_cached_source": "builtin" + }, + { + "id": "deposit", + "pg_provider": null, + "sort_order": 7, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "예치금결제", + "en": "Store Credit", + "fr": "예치금결제" + }, + "_cached_description": { + "ko": "예치금으로 결제", + "en": "Pay with store credit", + "fr": "예치금으로 결제" + }, + "_cached_icon": "wallet", + "_cached_source": "builtin" + }, + { + "id": "free", + "pg_provider": null, + "sort_order": 8, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "무료", + "en": "Free", + "fr": "무료" + }, + "_cached_description": { + "ko": "결제 없이 주문 완료", + "en": "Order without payment", + "fr": "결제 없이 주문 완료" + }, + "_cached_icon": "gift", + "_cached_source": "builtin" + } + ], + "banks": [], + "bank_accounts": [ + { + "bank_code": "004", + "account_number": "", + "account_holder": "", + "is_active": false, + "is_default": false + } + ], + "auto_cancel_expired": true, + "auto_cancel_days": 3, + "cart_expiry_days": 30, + "stock_restore_on_cancel": true, + "cancellable_statuses": [ + "payment_complete" + ], + "confirmable_statuses": [ + "shipping", + "delivered" + ] + }, + "shipping": { + "default_country": "KR", + "available_countries": [ + { + "code": "KR", + "name": { + "ko": "대한민국", + "en": "South Korea", + "fr": "대한민국" + }, + "is_active": true + }, + { + "code": "US", + "name": { + "ko": "미국", + "en": "United States", + "fr": "미국" + }, + "is_active": false + }, + { + "code": "JP", + "name": { + "ko": "일본", + "en": "Japan", + "fr": "일본" + }, + "is_active": false + }, + { + "code": "CN", + "name": { + "ko": "중국", + "en": "China", + "fr": "중국" + }, + "is_active": false + }, + { + "code": "SG", + "name": { + "ko": "싱가포르", + "en": "Singapore", + "fr": "싱가포르" + }, + "is_active": false + }, + { + "code": "HK", + "name": { + "ko": "홍콩", + "en": "Hong Kong", + "fr": "홍콩" + }, + "is_active": false + }, + { + "code": "TW", + "name": { + "ko": "대만", + "en": "Taiwan", + "fr": "대만" + }, + "is_active": false + }, + { + "code": "VN", + "name": { + "ko": "베트남", + "en": "Vietnam", + "fr": "베트남" + }, + "is_active": false + }, + { + "code": "TH", + "name": { + "ko": "태국", + "en": "Thailand", + "fr": "태국" + }, + "is_active": false + }, + { + "code": "MY", + "name": { + "ko": "말레이시아", + "en": "Malaysia", + "fr": "말레이시아" + }, + "is_active": false + } + ], + "international_shipping_enabled": false, + "free_shipping_threshold": 50000, + "free_shipping_enabled": true, + "address_validation_enabled": false, + "address_api_provider": "kakao" + }, + "seo": { + "meta_category_title": "{commerce_name} - {category_name}", + "meta_category_description": "", + "meta_search_title": "{commerce_name} - {keyword_name}", + "meta_search_description": "", + "meta_product_title": "{commerce_name} - {product_name}", + "meta_product_description": "", + "meta_shop_index_title": "{commerce_name}", + "meta_shop_index_description": "", + "seo_category": true, + "seo_search_result": true, + "seo_product_detail": true, + "seo_shop_index": true + }, + "review_settings": { + "write_deadline_days": 90, + "max_images": 5, + "max_image_size_mb": 10 + }, + "inquiry": { + "board_slug": null + }, + "notifications": { + "channels": [ + { + "id": "mail", + "is_active": true, + "sort_order": 1 + }, + { + "id": "database", + "is_active": true, + "sort_order": 2 + } + ] + }, + "mileage": { + "enabled": false, + "default_earn_rate": 1, + "earn_trigger": "confirmed", + "earn_delay_days": "0", + "currency_rules": [ + { + "currency_code": "KRW", + "point_value": 1, + "min_use_amount": 1000, + "use_unit": 10, + "max_use_type": "fixed", + "max_use_percent": 30, + "max_use_value": 50000 + } + ], + "expiry_enabled": true, + "expiry_days": 365, + "expiry_notification_enabled": true, + "expiry_notification_days_before": 7 + } + } +} +``` **에러 응답** @@ -136,10 +2604,23 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/settings/clear-cache HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -162,6 +2643,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/settings/seo-cache-info HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -172,6 +2662,24 @@ _단건 응답: `data` 객체의 필드._ | size_bytes | integer | `0` | 캐시된 SEO 페이지의 지원 로케일별 HTML 총 바이트 | | size_formatted | string | `0 B` | `size` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-ecommerce::messages.settings.fetch_success", + "data": { + "count": 0, + "size_bytes": 0, + "size_formatted": "0 B" + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -196,9 +2704,63 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | category | path | string | 예 | — | 분류 필터 (해당 분류의 항목만 조회) | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/settings/basic_info HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| category | string | `basic_info` | 조회한 설정 분류(탭) 식별자 (`basic_info`·`language_currency`·`shipping` 등 요청 경로의 category 값 반영) | +| settings | object | `{"shop_name":"","route_path":"shop","no_route":false,"com…` | 해당 분류의 설정 값 객체 (분류에 따라 구조가 달라지며, 배송·클레임 등은 DB 관리 대상이 병합되어 반환) | +| abilities | object | `{"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-ecommerce::messages.settings.fetch_success", + "data": { + "category": "basic_info", + "settings": { + "shop_name": "", + "route_path": "shop", + "no_route": false, + "company_name": "", + "business_number": "", + "ceo_name": "", + "business_type": "", + "business_category": "", + "zipcode": "", + "base_address": "", + "detail_address": "", + "phone": "", + "fax": "", + "email": "", + "privacy_officer": "", + "privacy_officer_email": "", + "mail_order_number": "", + "telecom_number": "" + }, + "abilities": { + "can_update": true + } + } +} +``` **에러 응답** @@ -223,6 +2785,14 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/settings/checkout HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -232,6 +2802,315 @@ _단건 응답: `data` 객체의 필드._ | shipping | object | `{"default_country":"KR","available_countries":[{"code":"K…` | 체크아웃용 배송 설정 (기본 국가·배송 가능 국가·무료배송·배송유형 등) | | order_settings | object | `{"default_pg_provider":null,"payment_methods":[{"id":"car…` | 체크아웃용 주문/결제 설정 (기본 PG·활성 결제수단·무통장 계좌 등) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-ecommerce::messages.settings.fetch_success", + "data": { + "shipping": { + "default_country": "KR", + "available_countries": [ + { + "code": "KR", + "name": { + "ko": "대한민국", + "en": "South Korea", + "fr": "대한민국" + }, + "is_active": true + }, + { + "code": "US", + "name": { + "ko": "미국", + "en": "United States", + "fr": "미국" + }, + "is_active": false + }, + { + "code": "JP", + "name": { + "ko": "일본", + "en": "Japan", + "fr": "일본" + }, + "is_active": false + }, + { + "code": "CN", + "name": { + "ko": "중국", + "en": "China", + "fr": "중국" + }, + "is_active": false + }, + { + "code": "SG", + "name": { + "ko": "싱가포르", + "en": "Singapore", + "fr": "싱가포르" + }, + "is_active": false + }, + { + "code": "HK", + "name": { + "ko": "홍콩", + "en": "Hong Kong", + "fr": "홍콩" + }, + "is_active": false + }, + { + "code": "TW", + "name": { + "ko": "대만", + "en": "Taiwan", + "fr": "대만" + }, + "is_active": false + }, + { + "code": "VN", + "name": { + "ko": "베트남", + "en": "Vietnam", + "fr": "베트남" + }, + "is_active": false + }, + { + "code": "TH", + "name": { + "ko": "태국", + "en": "Thailand", + "fr": "태국" + }, + "is_active": false + }, + { + "code": "MY", + "name": { + "ko": "말레이시아", + "en": "Malaysia", + "fr": "말레이시아" + }, + "is_active": false + } + ], + "international_shipping_enabled": false, + "free_shipping_threshold": 50000, + "free_shipping_enabled": true, + "address_validation_enabled": false, + "address_api_provider": "kakao" + }, + "order_settings": { + "default_pg_provider": null, + "payment_methods": [ + { + "id": "card", + "pg_provider": null, + "sort_order": 1, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "신용카드", + "en": "Credit Card", + "fr": "신용카드" + }, + "_cached_description": { + "ko": "신용카드로 안전하게 결제", + "en": "Pay securely with credit card", + "fr": "신용카드로 안전하게 결제" + }, + "_cached_icon": "credit-card", + "_cached_source": "builtin" + }, + { + "id": "vbank", + "pg_provider": null, + "sort_order": 2, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "order_placed", + "_cached_name": { + "ko": "가상계좌", + "en": "Virtual Account", + "fr": "가상계좌" + }, + "_cached_description": { + "ko": "가상계좌로 입금", + "en": "Pay via virtual account", + "fr": "가상계좌로 입금" + }, + "_cached_icon": "money-check", + "_cached_source": "builtin" + }, + { + "id": "dbank", + "pg_provider": null, + "sort_order": 3, + "is_active": true, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "order_placed", + "_cached_name": { + "ko": "무통장입금", + "en": "Bank Transfer", + "fr": "무통장입금" + }, + "_cached_description": { + "ko": "지정 계좌로 직접 입금", + "en": "Direct bank transfer", + "fr": "지정 계좌로 직접 입금" + }, + "_cached_icon": "building-columns", + "_cached_source": "builtin" + }, + { + "id": "bank", + "pg_provider": null, + "sort_order": 4, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "계좌이체", + "en": "Account Transfer", + "fr": "계좌이체" + }, + "_cached_description": { + "ko": "실시간 계좌이체", + "en": "Real-time bank transfer", + "fr": "실시간 계좌이체" + }, + "_cached_icon": "building-columns", + "_cached_source": "builtin" + }, + { + "id": "phone", + "pg_provider": null, + "sort_order": 5, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "payment_complete", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "휴대폰결제", + "en": "Mobile Payment", + "fr": "휴대폰결제" + }, + "_cached_description": { + "ko": "휴대폰 소액결제", + "en": "Mobile phone payment", + "fr": "휴대폰 소액결제" + }, + "_cached_icon": "mobile-screen-button", + "_cached_source": "builtin" + }, + { + "id": "point", + "pg_provider": null, + "sort_order": 6, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "포인트결제", + "en": "Points", + "fr": "포인트결제" + }, + "_cached_description": { + "ko": "적립 포인트로 결제", + "en": "Pay with points", + "fr": "적립 포인트로 결제" + }, + "_cached_icon": "coins", + "_cached_source": "builtin" + }, + { + "id": "deposit", + "pg_provider": null, + "sort_order": 7, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "예치금결제", + "en": "Store Credit", + "fr": "예치금결제" + }, + "_cached_description": { + "ko": "예치금으로 결제", + "en": "Pay with store credit", + "fr": "예치금으로 결제" + }, + "_cached_icon": "wallet", + "_cached_source": "builtin" + }, + { + "id": "free", + "pg_provider": null, + "sort_order": 8, + "is_active": false, + "min_order_amount": 0, + "stock_deduction_timing": "order_placed", + "mileage_deduction_timing": "payment_complete", + "_cached_name": { + "ko": "무료", + "en": "Free", + "fr": "무료" + }, + "_cached_description": { + "ko": "결제 없이 주문 완료", + "en": "Order without payment", + "fr": "결제 없이 주문 완료" + }, + "_cached_icon": "gift", + "_cached_source": "builtin" + } + ], + "banks": [], + "bank_accounts": [ + { + "bank_code": "004", + "account_number": "", + "account_holder": "", + "is_active": false, + "is_default": false + } + ], + "auto_cancel_expired": true, + "auto_cancel_days": 3, + "cart_expiry_days": 30, + "stock_restore_on_cancel": true, + "cancellable_statuses": [ + "payment_complete" + ], + "confirmable_statuses": [ + "shipping", + "delivered" + ] + } + } +} +``` + **에러 응답** _대표 에러 없음 (공개 조회). _ @@ -251,6 +3130,14 @@ _대표 에러 없음 (공개 조회). _ @@ -278,6 +3375,14 @@ _대표 에러 없음 (공개 조회). _ @@ -305,6 +3430,14 @@ _대표 에러 없음 (공개 조회). _ diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-carriers.md b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-carriers.md index 889097c4..eac3aaa0 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-carriers.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-carriers.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/shipping-carriers HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -49,6 +58,77 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | updater | array | `[]` | 최종 수정자 정보 객체 (id/name — updater 관계 로드 시) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송사 목록을 조회했습니다.", + "data": { + "data": [ + { + "number": 13, + "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", + "creator": [], + "updater": [], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + }, + { + "number": 12, + "id": 1, + "code": "cj", + "name": { + "ko": "CJ대한통운", + "en": "CJ Logistics" + }, + "localized_name": "CJ대한통운", + "type": "domestic", + "type_label": "국내", + "tracking_url": "https://trace.cjlogistics.com/next/tracking.html?wblNo={tracking_number}", + "is_active": true, + "sort_order": 1, + "created_at": "2026-07-08 10:43:32", + "updated_at": "2026-07-08 10:43:32", + "creator": [], + "updater": [], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + }, + "... (총 13건 중 2건 표시)" + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -80,10 +160,35 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_carrier.create_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/shipping-carriers HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "code": "예시값", + "name": [ + "예시 이름" + ], + "type": "예시값", + "tracking_url": "https://example.com", + "is_active": true, + "sort_order": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -107,6 +212,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/shipping-carriers/active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -117,6 +231,112 @@ _요청 파라미터 없음._ | type | string | `domestic` | 배송사 유형 (`domestic` 국내 / `international` 해외) | | tracking_url | string | `https://trace.cjlogistics.com/next/tr…` | tracking URL | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송사 목록을 조회했습니다.", + "data": [ + { + "value": 13, + "label": "API 문서 샘플 배송사", + "code": "apidoc", + "type": "domestic", + "tracking_url": null + }, + { + "value": 1, + "label": "CJ대한통운", + "code": "cj", + "type": "domestic", + "tracking_url": "https://trace.cjlogistics.com/next/tracking.html?wblNo={tracking_number}" + }, + { + "value": 2, + "label": "한진택배", + "code": "hanjin", + "type": "domestic", + "tracking_url": "https://www.hanjin.com/kor/CMS/DeliveryMgr/WaybillResult.do?wblnb={tracking_number}" + }, + { + "value": 3, + "label": "롯데택배", + "code": "lotte", + "type": "domestic", + "tracking_url": "https://www.lotteglogis.com/home/reservation/tracking/link498?InvNo={tracking_number}" + }, + { + "value": 4, + "label": "로젠택배", + "code": "logen", + "type": "domestic", + "tracking_url": "https://www.ilogen.com/web/personal/trace/{tracking_number}" + }, + { + "value": 5, + "label": "UPS", + "code": "ups", + "type": "international", + "tracking_url": "https://www.ups.com/track?tracknum={tracking_number}" + }, + { + "value": 6, + "label": "EMS", + "code": "ems", + "type": "international", + "tracking_url": "https://service.epost.go.kr/trace.RetrieveEmsRi498.postal?POST_CODE={tracking_number}" + }, + { + "value": 7, + "label": "DHL", + "code": "dhl", + "type": "international", + "tracking_url": "https://www.dhl.com/kr-ko/home/tracking/tracking-express.html?submit=1&tracking-id={tracking_number}" + }, + { + "value": 8, + "label": "FedEx", + "code": "fedex", + "type": "international", + "tracking_url": "https://www.fedex.com/fedextrack/?tracknumbers={tracking_number}" + }, + { + "value": 9, + "label": "SF Express", + "code": "sf", + "type": "international", + "tracking_url": null + }, + { + "value": 10, + "label": "야마토운수", + "code": "yamato", + "type": "international", + "tracking_url": null + }, + { + "value": 11, + "label": "사가와익스프레스", + "code": "sagawa", + "type": "international", + "tracking_url": null + }, + { + "value": 12, + "label": "기타", + "code": "other", + "type": "domestic", + "tracking_url": null + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -141,9 +361,38 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/shipping-carriers/13 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| carrier_id | integer | `13` | carrier 식별자 (연관 리소스 참조) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송사가 삭제되었습니다.", + "data": { + "carrier_id": 13 + } +} +``` **에러 응답** @@ -170,9 +419,67 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/shipping-carriers/13 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| 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` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| tracking_url | null | `null` | tracking URL | +| is_active | boolean | `true` | active 여부 | +| 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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 10:44:49", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -207,10 +514,35 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_carrier.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/shipping-carriers/13 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "code": "예시값", + "name": [ + "예시 이름" + ], + "type": "예시값", + "tracking_url": "https://example.com", + "is_active": true, + "sort_order": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -237,9 +569,67 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/shipping-carriers/13/toggle-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| 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` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| tracking_url | null | `null` | tracking URL | +| is_active | boolean | `false` | active 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| created_at | string | `2026-07-08 10:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 15:00:34` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-ecommerce::messages.shipping_carriers.status_changed", + "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": false, + "sort_order": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:00:34", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-country.md b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-country.md index 0d30179e..cf4da12e 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-country.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-country.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/user/shipping-country HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -35,6 +44,22 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | preferred_shipping_country | null | `null` | 회원이 저장한 선호 배송국가 코드 (2자리 대문자, 미설정 시 `null`) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송국가를 조회했습니다.", + "data": { + "preferred_shipping_country": null + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -58,9 +83,43 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | shipping_country | body | string | 예 | — | 저장할 선호 배송국가 2자리 코드 (활성 배송가능 국가만 허용, 대문자로 정규화되어 저장) | +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/user/shipping-country HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "shipping_country": "KR" +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| preferred_shipping_country | string | `KR` | 변경 후 저장된 회원 선호 배송국가 코드 (2자리 대문자 ISO 국가코드) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송국가가 변경되었습니다.", + "data": { + "preferred_shipping_country": "KR" + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-policies.md b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-policies.md index 33f1c20c..3178aa0f 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-policies.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-policies.md @@ -39,6 +39,15 @@ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_policy.list_validation_rules`, `sirsoft-ecommerce.shipping_policy.list_validation_messages`). +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/shipping-policies?search=%EC%98%88%EC%8B%9C%EA%B0%92&shipping_methods=%EC%98%88%EC%8B%9C%EA%B0%92&charge_policies=%EC%98%88%EC%8B%9C%EA%B0%92&countries=KR&is_active=%2C%20&sort_by=id&sort_order=asc&per_page=1&page=1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -59,6 +68,66 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송정책 목록을 조회했습니다.", + "data": { + "data": [ + { + "number": 1, + "id": 1, + "name": { + "ko": "API 문서 샘플 배송정책", + "en": "API Doc Sample Shipping Policy" + }, + "name_localized": "API 문서 샘플 배송정책", + "country_settings": [], + "fee_summary": "", + "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 + } + } + ], + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + }, + "statistics": { + "total": 1, + "active": 1, + "inactive": 0, + "shipping_method": [], + "charge_policy": [] + }, + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -90,10 +159,36 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_policy.store_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/shipping-policies HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "is_active": true, + "is_default": true, + "sort_order": 1, + "country_settings": [ + "KR" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -117,6 +212,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/shipping-policies/active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -127,6 +231,28 @@ _요청 파라미터 없음._ | fee_summary | string | `KR: 외부 API 연동 (실시간 계산)` | 국가별 배송비 요약 텍스트 (`country_code: fee` 형태를 ` \| ` 로 결합) | | is_default | boolean | `false` | default 여부 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-ecommerce::messages.shipping_policy.active_list_retrieved", + "data": [ + { + "value": 1, + "label": "API 문서 샘플 배송정책", + "countries_display": "", + "fee_summary": "", + "is_default": false + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -153,10 +279,23 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_policy.bulk_delete_validation_rules`). +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/shipping-policies/bulk?ids=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -185,10 +324,31 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_policy.bulk_toggle_active_validation_rules`). +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/shipping-policies/bulk-toggle-active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "is_active": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -217,10 +377,37 @@ _요청 파라미터 없음._ | config | body | array | 아니오 | — | API 호출 고급 설정 (HTTP 메서드·인증방식·필드 매핑·응답 형식/경로 등) | | sample | body | array | 아니오 | — | 테스트 계산에 사용할 샘플 주문 데이터 (무게/금액/수량 등) | +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/admin/shipping-policies/test-api-call HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "endpoint": "예시값", + "request_fields": [ + "예시값" + ], + "config": [ + "예시값" + ], + "sample": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -246,9 +433,34 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/admin/shipping-policies/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송정책이 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -275,9 +487,67 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/admin/shipping-policies/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 배송정책","en":"API Doc Sample Shipping Poli…` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| name_localized | string | `API 문서 샘플 배송정책` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| country_settings | array | `[]` | 국가별 배송 설정 목록 (countrySettings 관계 로드 시 각 국가의 배송방식·부과정책·배송비 상세) | +| fee_summary | string | `` | 활성 국가별 설정을 종합한 배송비 요약 텍스트 (예: `KR: 3000원 \| US: $20`, 활성 설정 없으면 빈 문자열) | +| 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송정책 정보를 조회했습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 배송정책", + "en": "API Doc Sample Shipping Policy" + }, + "name_localized": "API 문서 샘플 배송정책", + "country_settings": [], + "fee_summary": "", + "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 + } + } +} +``` **에러 응답** @@ -311,10 +581,36 @@ _요청 파라미터 없음._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_policy.update_validation_rules`). +**요청 예시** + +```http +PUT /api/modules/sirsoft-ecommerce/admin/shipping-policies/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "name": [ + "예시 이름" + ], + "is_active": true, + "is_default": true, + "sort_order": 1, + "country_settings": [ + "KR" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -341,9 +637,65 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/shipping-policies/1/set-default HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 배송정책","en":"API Doc Sample Shipping Poli…` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| name_localized | string | `API 문서 샘플 배송정책` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| fee_summary | string | `` | 활성 국가별 설정을 종합한 배송비 요약 텍스트 (예: `KR: 3000원 \| US: $20`, 활성 설정 없으면 빈 문자열) | +| countries_display | string | `` | 활성 배송 국가를 국기 이모지로 표시한 문자열 (최대 3개 노출, 초과분은 `+N` 축약) | +| is_active | boolean | `true` | active 여부 | +| is_default | boolean | `true` | 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "기본 배송정책이 설정되었습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 배송정책", + "en": "API Doc Sample Shipping Policy" + }, + "name_localized": "API 문서 샘플 배송정책", + "fee_summary": "", + "countries_display": "", + "is_active": true, + "is_default": true, + "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 + } + } +} +``` **에러 응답** @@ -370,9 +722,65 @@ _요청 파라미터 없음._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/shipping-policies/1/toggle-active HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 배송정책","en":"API Doc Sample Shipping Poli…` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| name_localized | string | `API 문서 샘플 배송정책` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| fee_summary | string | `` | 활성 국가별 설정을 종합한 배송비 요약 텍스트 (예: `KR: 3000원 \| US: $20`, 활성 설정 없으면 빈 문자열) | +| countries_display | string | `` | 활성 배송 국가를 국기 이모지로 표시한 문자열 (최대 3개 노출, 초과분은 `+N` 축약) | +| is_active | boolean | `false` | 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}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송정책 사용여부가 변경되었습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 배송정책", + "en": "API Doc Sample Shipping Policy" + }, + "name_localized": "API 문서 샘플 배송정책", + "fee_summary": "", + "countries_display": "", + "is_active": false, + "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 + } + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/users.md b/modules/_bundled/sirsoft-ecommerce/docs/api/users.md index 45a6639a..70877bc8 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/users.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/users.md @@ -30,10 +30,28 @@ | user | path | string | 예 | — | 대상 user의 식별자 | | currency | body | string | 예 | — | 회원에게 지정할 결제 통화 코드 (등록 통화: is_default 또는 exchange_rate>0 인 통화만 허용) | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/users/a234c2b1-cde8-437f-b28b-23323be2b98d/currency HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "currency": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -61,9 +79,43 @@ | user | path | string | 예 | — | 대상 user의 식별자 | | shipping_country | body | string | 예 | — | 회원에게 지정할 배송국가 2자리 코드 (활성 배송가능 국가만 허용, 대문자로 정규화) | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-ecommerce/admin/users/a234c2b1-cde8-437f-b28b-23323be2b98d/shipping-country HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "shipping_country": "KR" +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| preferred_shipping_country | string | `KR` | 변경 후 저장된 회원 선호 배송국가 코드 (2자리 대문자 ISO 국가코드) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송국가가 변경되었습니다.", + "data": { + "preferred_shipping_country": "KR" + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/wishlist.md b/modules/_bundled/sirsoft-ecommerce/docs/api/wishlist.md index 1b099ed6..b5392d1f 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/wishlist.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/wishlist.md @@ -27,12 +27,46 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/modules/sirsoft-ecommerce/wishlist HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "찜 목록을 불러왔습니다.", + "data": { + "data": [], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 0, + "from": null, + "to": null, + "has_more_pages": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -58,9 +92,43 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.wishlist.toggle_validation_rules`). +**요청 예시** + +```http +POST /api/modules/sirsoft-ecommerce/wishlist/toggle HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "product_id": 1 +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| added | boolean | `true` | 토글 후 찜 상태 (true 이면 찜 목록에 추가됨 / false 이면 제거됨) | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품이 찜 목록에 추가되었습니다.", + "data": { + "added": true + } +} +``` **에러 응답** @@ -86,10 +154,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-ecommerce/wishlist/{id} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-page/docs/api/attachments.md b/modules/_bundled/sirsoft-page/docs/api/attachments.md index c7565987..982f3db6 100644 --- a/modules/_bundled/sirsoft-page/docs/api/attachments.md +++ b/modules/_bundled/sirsoft-page/docs/api/attachments.md @@ -32,10 +32,43 @@ | collection | body | string | 아니오 | max 100 | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) | | temp_key | body | string | 아니오 | max 64 | 저장 전 임시 귀속 키. 아직 저장되지 않은 페이지의 첨부를 임시로 묶어 두고, 이후 페이지 저장 시 이 키로 확정 귀속합니다 (`page_id` 미지정 시 사용) | +**요청 예시** + +```http +POST /api/modules/sirsoft-page/admin/attachments HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary +Content-Disposition: form-data; name="page_id" + +1 +------G7ExampleBoundary +Content-Disposition: form-data; name="collection" + +예시값 +------G7ExampleBoundary +Content-Disposition: form-data; name="temp_key" + +예시값 +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -61,10 +94,30 @@ | --- | --- | --- | --- | --- | --- | | order | body | array | 예 | min 1 | 새 정렬 순서 배열. 각 원소는 `{"id": 첨부ID(정수), "order": 순서값(0 이상 정수)}` 형태이며, 이 순서대로 첨부 표시 순위가 갱신됩니다 | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-page/admin/attachments/reorder HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "order": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -90,10 +143,23 @@ | --- | --- | --- | --- | --- | --- | | id | path | string | 예 | — | 대상 리소스의 식별자 | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-page/admin/attachments/{id} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/modules/_bundled/sirsoft-page/docs/api/pages.md b/modules/_bundled/sirsoft-page/docs/api/pages.md index 7e6925ce..2b586642 100644 --- a/modules/_bundled/sirsoft-page/docs/api/pages.md +++ b/modules/_bundled/sirsoft-page/docs/api/pages.md @@ -36,6 +36,15 @@ | sort_by | query | string | 아니오 | `created_at`, `published_at` | 정렬 기준 필드명 | | sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | +**요청 예시** + +```http +GET /api/modules/sirsoft-page/admin/pages?published=1&search=%EC%98%88%EC%8B%9C%EA%B0%92&search_field=all&filters=%EC%98%88%EC%8B%9C%EA%B0%92&per_page=1&page=1&sort_by=created_at&sort_order=asc HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -54,6 +63,72 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "페이지 정보를 조회했습니다.", + "data": { + "data": [ + { + "id": 10, + "slug": "apidoc-sample-page", + "title": "API 문서 샘플 페이지", + "published": true, + "published_at": "2026-07-08 10:51:59", + "current_version": 2, + "creator": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자" + }, + "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 + } + }, + { + "id": 4, + "slug": "terms", + "title": "이용약관", + "published": true, + "published_at": "2026-07-08 10:44:43", + "current_version": 1, + "creator": null, + "created_at": "2026-07-08 10:44:43", + "updated_at": "2026-07-08 10:44:43", + "is_owner": false, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + }, + "... (총 7건 중 2건 표시)" + ], + "meta": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 7 + }, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -85,10 +160,38 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | seo_meta | body | array | 아니오 | — | SEO 메타 정보 맵. 하위 키 `title`(max 255)·`description`(max 500)·`keywords`(max 500)를 담습니다 | | temp_key | body | string | 아니오 | max 64 | 저장 전 첨부 업로드 시 발급받은 임시 키. 생성된 페이지에 임시 첨부를 귀속시키는 데 사용합니다 | +**요청 예시** + +```http +POST /api/modules/sirsoft-page/admin/pages HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "slug": "example-key", + "title": [ + "예시 제목" + ], + "content": "예시 내용입니다.", + "content_mode": "html", + "published": true, + "seo_meta": [ + "예시값" + ], + "temp_key": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -115,10 +218,31 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | | published | body | boolean | 예 | — | 발행 여부 (발행된 항목만 필터) | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-page/admin/pages/bulk-publish HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [ + "예시값" + ], + "published": true +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -145,10 +269,29 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | slug | body | string | 예 | max 255 | URL 친화 식별자 (slug) | | exclude_id | body | integer | 아니오 | min 1 | exclude 식별자 | +**요청 예시** + +```http +POST /api/modules/sirsoft-page/admin/pages/check-slug HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "slug": "example-key", + "exclude_id": 1 +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -174,9 +317,34 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | page | path | string | 예 | — | 조회할 페이지 번호 (1부터 시작) | +**요청 예시** + +```http +DELETE /api/modules/sirsoft-page/admin/pages/10 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "페이지가 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -203,6 +371,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | page | path | string | 예 | — | 조회할 페이지 번호 (1부터 시작) | +**요청 예시** + +```http +GET /api/modules/sirsoft-page/admin/pages/10 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -226,6 +403,71 @@ _단건 응답: `data` 객체의 필드._ | is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | | abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +**응답 예시** + +```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": "

API 레퍼런스 실측용 완전 샘플 페이지 본문입니다.

", + "en": "

Complete sample page body for API reference probing.

" + }, + "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": 2, + "creator": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자" + }, + "updater": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자" + }, + "attachments": [ + { + "id": 4, + "hash": "ninrdtsmhhta", + "original_filename": "nesciunt.jpg", + "mime_type": "image/jpeg", + "size": 7124557, + "collection": "attachments", + "order": 0, + "is_image": true, + "download_url": "/api/modules/sirsoft-page/pages/attachment/ninrdtsmhhta", + "preview_url": "/api/modules/sirsoft-page/pages/attachment/ninrdtsmhhta/preview", + "created_at": "2026-07-08 10:51:59" + } + ], + "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 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -258,10 +500,38 @@ _단건 응답: `data` 객체의 필드._ | seo_meta | body | array | 아니오 | — | SEO 메타 정보 맵. 하위 키 `title`(max 255)·`description`(max 500)·`keywords`(max 500)를 담습니다 | | temp_key | body | string | 아니오 | max 64 | 저장 전 첨부 업로드 시 발급받은 임시 키. 새로 업로드한 첨부를 이 페이지에 귀속시키는 데 사용합니다 | +**요청 예시** + +```http +PUT /api/modules/sirsoft-page/admin/pages/10 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "slug": "example-key", + "title": [ + "예시 제목" + ], + "content": "예시 내용입니다.", + "content_mode": "html", + "published": true, + "seo_meta": [ + "예시값" + ], + "temp_key": "예시값" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -289,9 +559,81 @@ _단건 응답: `data` 객체의 필드._ | page | path | string | 예 | — | 조회할 페이지 번호 (1부터 시작) | | published | body | boolean | 예 | — | 발행 여부 (발행된 항목만 필터) | +**요청 예시** + +```http +PATCH /api/modules/sirsoft-page/admin/pages/10/publish HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "published": true +} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `10` | 기본 키 (내부 식별자) | +| slug | string | `apidoc-sample-page` | URL 친화 식별자 (slug) | +| title | object | `{"ko":"API 문서 샘플 페이지","en":"API Doc Sample Page"}` | 제목 | +| content | object | `{"ko":"

API 레퍼런스 실측용 완전 샘플 페이지 본문입니다.<\/p>","en":"

Co…` | 본문 내용 | +| content_mode | string | `html` | 본문 형식 (html, text) | +| published | boolean | `true` | 발행 여부 (true: 발행, false: 미발행) | +| published_at | string | `2026-07-08 15:03:15` | published 일시 | +| seo_meta | object | `{"title":"Dolorum et aut officia ipsam doloribus inventor…` | SEO 메타 정보 (title, description, keywords) | +| current_version | integer | `2` | 현재 버전 번호 | +| created_at | string | `2026-07-08 10:51:59` | 생성 일시 | +| updated_at | string | `2026-07-08 15:03:15` | 최종 수정 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**응답 예시** + +```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": "

API 레퍼런스 실측용 완전 샘플 페이지 본문입니다.

", + "en": "

Complete sample page body for API reference probing.

" + }, + "content_mode": "html", + "published": true, + "published_at": "2026-07-08 15:03:15", + "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": 2, + "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 + } + } +} +``` **에러 응답** @@ -319,6 +661,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | page | path | string | 예 | — | 조회할 페이지 번호 (1부터 시작) | +**요청 예시** + +```http +GET /api/modules/sirsoft-page/admin/pages/10/versions HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) | 필드 | 타입 | 실측 예시값 | 용도/설명 | @@ -334,6 +685,63 @@ _단건 응답: `data` 객체의 필드._ | creator | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | | created_at | string | `2026-07-06 23:57:18` | 생성 일시 | +**응답 예시** + +```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": "

수정 버전 본문.

", + "en": "

Revised version body.

" + }, + "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" + }, + { + "id": 13, + "page_id": 10, + "version": 1, + "title": { + "ko": "API 문서 샘플 페이지 (v1)", + "en": "API Doc Sample Page (v1)" + }, + "content": { + "ko": "

초기 버전 본문.

", + "en": "

Initial version body.

" + }, + "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" + } + ] +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -360,10 +768,23 @@ _단건 응답: `data` 객체의 필드._ | page | path | string | 예 | — | 조회할 페이지 번호 (1부터 시작) | | versionId | path | string | 예 | — | 대상 version의 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-page/admin/pages/10/versions/{versionId} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -390,10 +811,23 @@ _단건 응답: `data` 객체의 필드._ | page | path | string | 예 | — | 조회할 페이지 번호 (1부터 시작) | | versionId | path | string | 예 | — | 대상 version의 식별자 | +**요청 예시** + +```http +POST /api/modules/sirsoft-page/admin/pages/10/versions/{versionId}/restore HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -419,10 +853,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-page/pages/attachment/{hash} HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -446,10 +893,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | +**요청 예시** + +```http +GET /api/modules/sirsoft-page/pages/attachment/{hash}/preview HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -473,9 +933,60 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +**요청 예시** + +```http +GET /api/modules/sirsoft-page/pages/terms HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `4` | 기본 키 (내부 식별자) | +| slug | string | `terms` | URL 친화 식별자 (slug) | +| title | string | `이용약관` | 제목 | +| content | string | `

제1조 (목적)

\n

[이용약관의 목적을 입력하세요.]

\n\n

제2조 (정의)

\n

[주요 용어의 정의를 입력하세요.]

\n\n

제3조 (약관의 효력 및 변경)

\n

[약관의 효력 및 변경에 관한 내용을 입력하세요.]

\n\n

제4조 (서비스의 제공 및 변경)

\n

[서비스 제공 및 변경에 관한 내용을 입력하세요.]

\n\n

제5조 (회원의 의무)

\n

[회원의 의무에 관한 내용을 입력하세요.]

", + "content_mode": "html", + "is_preview": false, + "published_at": "2026-07-08 10:44:43", + "seo_meta": null, + "current_version": 1, + "attachments": [], + "created_at": "2026-07-08 10:44:43", + "updated_at": "2026-07-08 10:44:43" + } +} +``` **에러 응답** diff --git a/plugins/_bundled/sirsoft-ckeditor5/docs/api/images.md b/plugins/_bundled/sirsoft-ckeditor5/docs/api/images.md index 5b40deb9..f6c43dad 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/docs/api/images.md +++ b/plugins/_bundled/sirsoft-ckeditor5/docs/api/images.md @@ -29,6 +29,14 @@ | --- | --- | --- | --- | --- | --- | | hash | path | string | 예 | — | 업로드 시 발급된 12자리 소문자 16진수 이미지 해시(라우트 제약 `[a-f0-9]{12}`). 저장된 `` 의 마지막 경로 세그먼트로, 이 값으로 서빙할 이미지를 조회한다. | +**요청 예시** + +```http +GET /api/plugins/sirsoft-ckeditor5/images/a1b2c3d4e5f6 HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) diff --git a/plugins/_bundled/sirsoft-ckeditor5/docs/api/upload.md b/plugins/_bundled/sirsoft-ckeditor5/docs/api/upload.md index a8dfb862..5381be58 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/docs/api/upload.md +++ b/plugins/_bundled/sirsoft-ckeditor5/docs/api/upload.md @@ -29,6 +29,23 @@ | --- | --- | --- | --- | --- | --- | | upload | body | file | 예 | max 2048 | 에디터에 드롭/붙여넣은 이미지 파일 1개(multipart). 허용 MIME 은 `jpeg,jpg,png,gif,webp`, 최대 크기는 플러그인 설정 `imageMaxSizeMb`(기본 2MB) 로 결정된다. | +**요청 예시** + +```http +POST /api/plugins/sirsoft-ckeditor5/upload HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: multipart/form-data; boundary=----G7ExampleBoundary + +------G7ExampleBoundary +Content-Disposition: form-data; name="upload"; filename="example.pdf" +Content-Type: application/octet-stream + +(바이너리 파일 내용) +------G7ExampleBoundary-- +``` + **응답 필드** (`data` 내부) diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Support/ApiDoc/ApiDocSampleService.php b/plugins/_bundled/sirsoft-ckeditor5/src/Support/ApiDoc/ApiDocSampleService.php new file mode 100644 index 00000000..bc251f8b --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Support/ApiDoc/ApiDocSampleService.php @@ -0,0 +1,80 @@ +}> 도메인 => 대표 레코드 정보 + */ + public function seed(): array + { + if (! class_exists(Ckeditor5ImageUpload::class)) { + return []; + } + + $image = $this->seedImage(); + + $entry = [ + 'model' => Ckeditor5ImageUpload::class, + 'key' => 'hash', + 'value' => (string) $image->hash, + 'path_params' => [ + 'hash' => (string) $image->hash, + ], + ]; + + return [ + 'images' => $entry, + ]; + } + + /** + * 고정 해시의 이미지 업로드 레코드 1건을 멱등 생성합니다. + * + * @return Ckeditor5ImageUpload 샘플 이미지 업로드 + */ + private function seedImage(): Ckeditor5ImageUpload + { + $actor = User::query()->orderBy('id')->first(); + + return Ckeditor5ImageUpload::query()->firstOrCreate( + ['hash' => self::SAMPLE_HASH], + [ + 'original_name' => 'apidoc-sample.png', + 'file_path' => 'ckeditor5/apidoc-sample.png', + 'storage_disk' => 'public', + 'file_size' => 1024, + 'mime_type' => 'image/png', + 'uploaded_by' => $actor?->getKey(), + ] + ); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Support/ApiDocSampleServiceTest.php b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Support/ApiDocSampleServiceTest.php new file mode 100644 index 00000000..28b6a80e --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Support/ApiDocSampleServiceTest.php @@ -0,0 +1,83 @@ +create([ + 'email' => 'apidoc-sample-user@example.com', + 'name' => 'API 문서 샘플 사용자', + ]); + } + + /** + * 시더가 계약을 구현한다. + */ + public function test_seeder_implements_contract(): void + { + $this->assertInstanceOf(ApiDocSampleSeeder::class, new ApiDocSampleService); + } + + /** + * 시더가 고정 해시(라우트 제약 `[a-f0-9]{12}` 충족)의 이미지 대표 샘플을 생성한다. + */ + public function test_seed_creates_image_with_valid_hash(): void + { + (new ApiDocSampleService)->seed(); + + $image = Ckeditor5ImageUpload::query()->where('hash', self::SAMPLE_HASH)->first(); + + $this->assertNotNull($image); + $this->assertMatchesRegularExpression('/^[a-f0-9]{12}$/', $image->hash); + } + + /** + * 시더가 images 도메인의 path_params 맵을 반환한다. + */ + public function test_seed_returns_images_path_params_map(): void + { + $map = (new ApiDocSampleService)->seed(); + + $this->assertArrayHasKey('images', $map); + $entry = $map['images']; + $this->assertSame(Ckeditor5ImageUpload::class, $entry['model']); + $this->assertSame('hash', $entry['key']); + $this->assertArrayHasKey('hash', $entry['path_params']); + $this->assertSame(self::SAMPLE_HASH, $entry['path_params']['hash']); + } + + /** + * 재실행 시 이미지가 중복 생성되지 않는다 (멱등). + */ + public function test_seed_is_idempotent(): void + { + (new ApiDocSampleService)->seed(); + (new ApiDocSampleService)->seed(); + + $this->assertSame(1, Ckeditor5ImageUpload::query()->where('hash', self::SAMPLE_HASH)->count()); + } +} diff --git a/plugins/_bundled/sirsoft-gdpr/docs/api/consent-log.md b/plugins/_bundled/sirsoft-gdpr/docs/api/consent-log.md index 43b40ee1..305824e1 100644 --- a/plugins/_bundled/sirsoft-gdpr/docs/api/consent-log.md +++ b/plugins/_bundled/sirsoft-gdpr/docs/api/consent-log.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/sirsoft-gdpr/admin/consent-log HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -47,6 +56,30 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | created_at | string | `2026-07-03 00:24:27` | 생성 일시 | | user | object | `{"id":130,"uuid":"a21d03b4-df0b-4aa6-ab7a-f51143f6375a","…` | 동의 주체 회원 정보(id/uuid/name/email). 관계가 로드된 경우에만 포함되며 게스트 이력이거나 삭제로 익명화된 경우 null입니다 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "data": [], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 0, + "from": null, + "to": null + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/plugins/_bundled/sirsoft-gdpr/docs/api/consent.md b/plugins/_bundled/sirsoft-gdpr/docs/api/consent.md index 80cba515..fafef2f0 100644 --- a/plugins/_bundled/sirsoft-gdpr/docs/api/consent.md +++ b/plugins/_bundled/sirsoft-gdpr/docs/api/consent.md @@ -27,10 +27,23 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/plugins/sirsoft-gdpr/consent/cookie HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** _대표 에러 없음 (공개 조회). _ @@ -50,6 +63,15 @@ _대표 에러 없음 (공개 조회). _ @@ -81,10 +123,23 @@ _대표 에러 없음 (공개 조회). +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -106,6 +161,15 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/sirsoft-gdpr/consent/history HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -114,6 +178,22 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | histories | array | `[]` | 회원 본인의 동의 변경 이력 배열. 각 항목은 `consent_key`, `action`(granted/revoked), `source`, `policy_version`, `categories`, `created_at`로 구성되며 부여/철회 기록을 시간순으로 제공합니다 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "histories": [] + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -135,6 +215,15 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/sirsoft-gdpr/consent/me HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -146,6 +235,94 @@ _단건 응답: `data` 객체의 필드._ | current_policy_version | string | `10` | 현재 발행된 최신 정책 버전 문자열. 각 동의 항목의 `policy_version`과 비교해 항목별 갱신 필요 여부를 계산하는 기준입니다 | | consents | array | `[{"id":null,"consent_key":"cookie_necessary","consent_lab…` | 카탈로그의 모든 쿠키 카테고리와 회원 status를 합친 동의 매트릭스. 항목마다 다국어 라벨(`consent_label`), 필수 여부(`is_required`), 현재 동의 상태(`is_consented`), 철회/재동의 가능 여부(`can_revoke`/`can_grant`), 항목별 갱신 필요(`needs_renewal_this_item`)를 담아 한 화면에서 철회·재동의·신규 동의를 처리하게 합니다 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "user_id": 6, + "needs_renewal": false, + "current_policy_version": "1", + "consents": [ + { + "id": null, + "consent_key": "cookie_necessary", + "consent_label": "필수 쿠키", + "consent_description": "세션·CSRF·로그인 토큰, 장바구니 식별자, 사용자가 가입 시 선택한 언어 설정, 쿠키 동의 기록 등 사이트 운영에 반드시 필요한 항목입니다. 비활성화할 수 없습니다.", + "consent_category": "necessary", + "is_required": true, + "is_consented": false, + "can_revoke": false, + "can_grant": false, + "needs_renewal_this_item": false, + "consented_at": null, + "revoked_at": null, + "consent_count": 0, + "policy_version": null, + "last_source": null + }, + { + "id": null, + "consent_key": "cookie_functional", + "consent_label": "기능 쿠키", + "consent_description": "사용자 선호도(다크모드, 표시 통화 등)를 기억하는 쿠키입니다. 거부 시 매 방문마다 기본값으로 표시됩니다.", + "consent_category": "functional", + "is_required": false, + "is_consented": false, + "can_revoke": false, + "can_grant": true, + "needs_renewal_this_item": false, + "consented_at": null, + "revoked_at": null, + "consent_count": 0, + "policy_version": null, + "last_source": null + }, + { + "id": null, + "consent_key": "cookie_analytics", + "consent_label": "분석 쿠키", + "consent_description": "방문자가 사이트를 어떻게 이용하는지 익명으로 측정해 더 나은 서비스를 만드는 데 사용됩니다. (예: Google Analytics, Hotjar)", + "consent_category": "analytics", + "is_required": false, + "is_consented": false, + "can_revoke": false, + "can_grant": true, + "needs_renewal_this_item": false, + "consented_at": null, + "revoked_at": null, + "consent_count": 0, + "policy_version": null, + "last_source": null + }, + { + "id": null, + "consent_key": "cookie_marketing", + "consent_label": "마케팅 쿠키", + "consent_description": "관심사에 맞는 광고를 보여주거나, 광고가 얼마나 효과적이었는지 측정하는 데 사용됩니다. SNS 영상 임베드 등도 포함됩니다. (예: Facebook 픽셀, Google 광고, YouTube 영상)", + "consent_category": "marketing", + "is_required": false, + "is_consented": false, + "can_revoke": false, + "can_grant": true, + "needs_renewal_this_item": false, + "consented_at": null, + "revoked_at": null, + "consent_count": 0, + "policy_version": null, + "last_source": null + } + ] + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -167,9 +344,38 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/plugins/sirsoft-gdpr/consent/renew-all HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| renewed | integer | `0` | 이번 호출로 현재 정책 버전으로 재동의 처리된 활성 선택형 동의 항목 수. 필수 쿠키와 이미 철회한 항목·이미 현재 버전인 항목은 제외되며, 프론트 toast 메시지의 `{renewed}` 보간값으로 사용됩니다 | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "활성 항목 {renewed} 개를 새 정책 버전으로 갱신했습니다.", + "data": { + "renewed": 0 + } +} +``` **에러 응답** @@ -192,10 +398,23 @@ _요청 파라미터 없음._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/plugins/sirsoft-gdpr/consent/revoke HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/plugins/_bundled/sirsoft-gdpr/docs/api/policy-versions.md b/plugins/_bundled/sirsoft-gdpr/docs/api/policy-versions.md index 886b561e..ba69f25f 100644 --- a/plugins/_bundled/sirsoft-gdpr/docs/api/policy-versions.md +++ b/plugins/_bundled/sirsoft-gdpr/docs/api/policy-versions.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/sirsoft-gdpr/admin/policy-versions HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ @@ -40,6 +49,39 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | created_at | string | `2026-06-18 09:27:39` | 생성 일시 | | publisher | object | `{"uuid":"a1e0a91a-fba6-491c-a53e-7285a5686857","name":"관리…` | 이 버전을 발행한 운영자 정보(uuid/name/email). raw FK(created_by)는 노출하지 않고 관계가 로드된 경우에만 포함되며, 발행자가 없으면 null입니다 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "data": [ + { + "id": 1, + "version": 1, + "change_type": "initial", + "memo": null, + "created_at": "2026-07-07 16:38:26", + "publisher": null + } + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 25, + "total": 1, + "from": 1, + "to": 1 + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -64,9 +106,49 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | --- | --- | --- | --- | --- | --- | | memo | body | string | 예 | min 1, max 500 | 발행 사유 메모(1~500자, 필수). 자동 감지 밖의 변경을 인지하고 명시적으로 새 버전을 발행할 때 감사 추적용으로 남기는 설명입니다 | +**요청 예시** + +```http +POST /api/plugins/sirsoft-gdpr/admin/policy-versions HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "memo": "예시값" +} +``` + **응답 필드** (`data` 내부) - +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| uuid | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) | +| name | string | `API 문서 샘플 사용자` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| email | string | `apidoc-sample-user@example.com` | 이 버전을 발행한 운영자의 이메일. publisher 관계에서 노출되며 감사 화면에서 발행자를 식별하는 용도입니다 | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "새 정책 버전이 발행되었습니다. 모든 회원이 다음 방문 시 재동의를 진행합니다.", + "data": { + "data": { + "id": 6, + "version": 2, + "0": "... (총 6건 중 2건 표시)" + } + } +} +``` **에러 응답** @@ -91,6 +173,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/sirsoft-gdpr/admin/policy-versions/current HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _목록 응답: `data.data[]` 배열 항목의 필드._ @@ -101,6 +192,26 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | name | string | `관리자` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | | email | string | `heuristing@gmail.com` | 이 버전을 발행한 운영자의 이메일. publisher 관계에서 노출되며 감사 화면에서 발행자를 식별하는 용도입니다 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "data": { + "id": 1, + "version": 1, + "0": "... (총 6건 중 2건 표시)" + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -125,9 +236,48 @@ _목록 응답: `data.data[]` 배열 항목의 필드._ | --- | --- | --- | --- | --- | --- | | version | path | string | 예 | — | 대상 버전 (버전 문자열) | +**요청 예시** + +```http +GET /api/plugins/sirsoft-gdpr/admin/policy-versions/1 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| banner_enabled | boolean | `true` | 발행 시점 스냅샷의 쿠키 배너·자동 차단 통합 활성 여부. `true`일 때만 게스트에게 배너가 노출되고 도메인 차단 엔진이 동작합니다 | +| banner_position | string | `bottom_bar` | 발행 시점의 쿠키 배너 노출 위치(bottom_bar / bottom_left_popup / bottom_right_popup / centered_modal) | +| blocked_domains | object | `{"analytics":["google-analytics.com","*.google-analytics.…` | 발행 시점에 설정된 카테고리별 차단 도메인 패턴(키→도메인 배열). 동의 전 로드를 차단하는 대상이며, 분쟁 시 당시 어떤 도메인이 차단 대상이었는지 확인하는 근거입니다 | +| cookie_categories | array | `[{"key":"necessary","label":{"en":"Strictly Necessary","k…` | 발행 시점의 쿠키 카테고리 카탈로그 배열. 각 항목은 `key`, 필수 여부, 다국어 `label`/`description`을 담으며, 회원 분쟁 시 당시 동의 대상 카테고리 본문을 확인하는 근거가 됩니다 | +| legal_entity_name | string | `` | 발행 시점에 표기된 개인정보 처리 법인/사업자명. 미설정 시 빈 문자열입니다 | +| privacy_policy_slug | string | `privacy` | 발행 시점의 개인정보 처리방침 페이지 slug. 배너/마이페이지의 방침 링크 대상이며, 미설정 시 빈 값입니다 | +| data_storage_location | string | `` | 발행 시점에 표기된 데이터 저장 위치 문자열(국가 단위 안내). 미설정 시 빈 문자열입니다 | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "data": { + "id": 1, + "version": 1, + "0": "... (총 8건 중 2건 표시)" + } + } +} +``` **에러 응답** diff --git a/plugins/_bundled/sirsoft-gdpr/docs/api/settings.md b/plugins/_bundled/sirsoft-gdpr/docs/api/settings.md index 7d2fab42..ac265354 100644 --- a/plugins/_bundled/sirsoft-gdpr/docs/api/settings.md +++ b/plugins/_bundled/sirsoft-gdpr/docs/api/settings.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/sirsoft-gdpr/admin/settings HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -35,6 +44,90 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | | settings | object | `{"privacy_policy_slug":"privacy","legal_entity_name":"","…` | GDPR 플러그인의 관리자 설정 전체 객체. 정책 메타데이터(slug/법인명/저장 위치), 배너 설정, 쿠키 카테고리 카탈로그, 차단 도메인을 담으며 `cookie_categories` 등 JSON 필드는 디코드되어 객체/배열로 노출됩니다 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "settings": { + "privacy_policy_slug": "privacy", + "legal_entity_name": "", + "data_storage_location": "", + "banner_enabled": true, + "banner_position": "bottom_bar", + "blocked_domains": { + "functional": [ + "*.crisp.chat", + "widget.intercom.io" + ], + "analytics": [ + "google-analytics.com" + ], + "marketing": [ + "facebook.com" + ] + }, + "cookie_categories": [ + { + "key": "necessary", + "required": true, + "label": { + "ko": "필수 쿠키", + "en": "Strictly Necessary" + }, + "description": { + "ko": "세션·CSRF·로그인 토큰, 장바구니 식별자, 사용자가 가입 시 선택한 언어 설정, 쿠키 동의 기록 등 사이트 운영에 반드시 필요한 항목입니다. 비활성화할 수 없습니다.", + "en": "Strictly necessary for site operation: session/CSRF/auth tokens, shopping basket identifier, user-selected language preference at registration, cookie consent record. Cannot be disabled." + } + }, + { + "key": "functional", + "required": false, + "label": { + "ko": "기능 쿠키", + "en": "Functional" + }, + "description": { + "ko": "사용자 선호도(다크모드, 표시 통화 등)를 기억하는 쿠키입니다. 거부 시 매 방문마다 기본값으로 표시됩니다.", + "en": "Cookies that remember user preferences such as dark mode and display currency. If declined, defaults are used on every visit." + } + }, + { + "key": "analytics", + "required": false, + "label": { + "ko": "분석 쿠키", + "en": "Analytics" + }, + "description": { + "ko": "방문자가 사이트를 어떻게 이용하는지 익명으로 측정해 더 나은 서비스를 만드는 데 사용됩니다. (예: Google Analytics, Hotjar)", + "en": "Used to anonymously measure how visitors use the site so we can improve it. (e.g. Google Analytics, Hotjar)" + } + }, + { + "key": "marketing", + "required": false, + "label": { + "ko": "마케팅 쿠키", + "en": "Marketing" + }, + "description": { + "ko": "관심사에 맞는 광고를 보여주거나, 광고가 얼마나 효과적이었는지 측정하는 데 사용됩니다. SNS 영상 임베드 등도 포함됩니다. (예: Facebook 픽셀, Google 광고, YouTube 영상)", + "en": "Used to show ads relevant to your interests, measure ad performance, and embed social media content. (e.g. Facebook Pixel, Google Ads, YouTube embeds)" + } + } + ] + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -65,10 +158,36 @@ _단건 응답: `data` 객체의 필드._ | cookie_categories | body | string | 아니오 | — | 쿠키 카테고리 카탈로그(JSON 문자열 또는 배열). 각 항목의 `key`는 necessary/functional/analytics/marketing 중 하나이며 `label.ko`/`label.en`이 필수입니다 | | blocked_domains | body | array | 아니오 | — | 카테고리별 차단 도메인 패턴 배열(necessary 제외). FQDN 및 `*.` 와일드카드 prefix만 허용되며 textarea 줄바꿈 입력도 배열로 정규화됩니다 | +**요청 예시** + +```http +PUT /api/plugins/sirsoft-gdpr/admin/settings HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "privacy_policy_slug": "example-key", + "legal_entity_name": "예시 이름", + "data_storage_location": "예시값", + "banner_enabled": true, + "banner_position": "bottom_bar", + "cookie_categories": "예시값", + "blocked_domains": [ + "예시값" + ] +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -92,6 +211,14 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/sirsoft-gdpr/settings HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -109,6 +236,144 @@ _단건 응답: `data` 객체의 필드._ | blocked_domains | object | `{"functional":["*.crisp.chat","widget.intercom.io"],"anal…` | 현재 설정된 카테고리별 차단 도메인 패턴(키→도메인 배열). 게스트도 차단이 동작해야 하므로 공개 응답에 노출되며 본 컨트롤러가 노출 SSoT입니다 | | default_blocked_domains_preview | object | `{"functional":["*.crisp.chat","client.crisp.chat","*.inte…` | 플러그인 기본 차단 도메인 카탈로그 미리보기(변경 불가 기본값). 관리자가 커스텀 차단 목록을 편집할 때 참고용 기본 패턴을 보여줍니다 | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "cookie_policy_version": "1", + "privacy_policy_slug": "privacy", + "privacy_policy_available": true, + "legal_entity_name": "", + "data_storage_location": "", + "banner_enabled": true, + "banner_position": "bottom_bar", + "cookie_categories": [ + { + "key": "necessary", + "required": true, + "label": { + "ko": "필수 쿠키", + "en": "Strictly Necessary" + }, + "description": { + "ko": "세션·CSRF·로그인 토큰, 장바구니 식별자, 사용자가 가입 시 선택한 언어 설정, 쿠키 동의 기록 등 사이트 운영에 반드시 필요한 항목입니다. 비활성화할 수 없습니다.", + "en": "Strictly necessary for site operation: session/CSRF/auth tokens, shopping basket identifier, user-selected language preference at registration, cookie consent record. Cannot be disabled." + } + }, + { + "key": "functional", + "required": false, + "label": { + "ko": "기능 쿠키", + "en": "Functional" + }, + "description": { + "ko": "사용자 선호도(다크모드, 표시 통화 등)를 기억하는 쿠키입니다. 거부 시 매 방문마다 기본값으로 표시됩니다.", + "en": "Cookies that remember user preferences such as dark mode and display currency. If declined, defaults are used on every visit." + } + }, + { + "key": "analytics", + "required": false, + "label": { + "ko": "분석 쿠키", + "en": "Analytics" + }, + "description": { + "ko": "방문자가 사이트를 어떻게 이용하는지 익명으로 측정해 더 나은 서비스를 만드는 데 사용됩니다. (예: Google Analytics, Hotjar)", + "en": "Used to anonymously measure how visitors use the site so we can improve it. (e.g. Google Analytics, Hotjar)" + } + }, + { + "key": "marketing", + "required": false, + "label": { + "ko": "마케팅 쿠키", + "en": "Marketing" + }, + "description": { + "ko": "관심사에 맞는 광고를 보여주거나, 광고가 얼마나 효과적이었는지 측정하는 데 사용됩니다. SNS 영상 임베드 등도 포함됩니다. (예: Facebook 픽셀, Google 광고, YouTube 영상)", + "en": "Used to show ads relevant to your interests, measure ad performance, and embed social media content. (e.g. Facebook Pixel, Google Ads, YouTube embeds)" + } + } + ], + "blocked_domains": { + "functional": [ + "*.crisp.chat", + "widget.intercom.io" + ], + "analytics": [ + "google-analytics.com" + ], + "marketing": [ + "facebook.com" + ] + }, + "default_blocked_domains_preview": { + "functional": [ + "*.crisp.chat", + "client.crisp.chat", + "*.intercom.io", + "widget.intercom.io", + "*.tawk.to", + "embed.tawk.to", + "cdn.weglot.com", + "*.weglot.com", + "*.usercentrics.eu" + ], + "analytics": [ + "google-analytics.com", + "*.google-analytics.com", + "googletagmanager.com", + "*.googletagmanager.com", + "ssl.google-analytics.com", + "*.hotjar.com", + "static.hotjar.com", + "*.mixpanel.com", + "cdn.mxpnl.com", + "*.amplitude.com", + "cdn.amplitude.com", + "*.segment.io", + "*.segment.com", + "wcs.naver.net", + "wcs.naver.com", + "*.beusable.net" + ], + "marketing": [ + "facebook.net", + "connect.facebook.net", + "facebook.com", + "*.facebook.com", + "doubleclick.net", + "*.doubleclick.net", + "googleadservices.com", + "googlesyndication.com", + "ads.google.com", + "*.criteo.com", + "static.criteo.net", + "*.adnxs.com", + "*.taboola.com", + "cdn.taboola.com", + "*.outbrain.com", + "*.kakao.com", + "analytics.ad.daum.net", + "platform.twitter.com", + "*.twitter.com", + "platform.linkedin.com", + "*.linkedin.com" + ] + } + } +} +``` + **에러 응답** _대표 에러 없음 (공개 조회). _ diff --git a/plugins/_bundled/sirsoft-gdpr/src/Support/ApiDoc/ApiDocSampleService.php b/plugins/_bundled/sirsoft-gdpr/src/Support/ApiDoc/ApiDocSampleService.php new file mode 100644 index 00000000..bba24208 --- /dev/null +++ b/plugins/_bundled/sirsoft-gdpr/src/Support/ApiDoc/ApiDocSampleService.php @@ -0,0 +1,79 @@ +}> 도메인 => 대표 레코드 정보 + */ + public function seed(): array + { + if (! class_exists(GdprPolicyVersion::class)) { + return []; + } + + $version = $this->seedPolicyVersion(); + + $entry = [ + 'model' => GdprPolicyVersion::class, + 'key' => 'version', + 'value' => (string) $version->version, + 'path_params' => [ + 'version' => (string) $version->version, + ], + ]; + + return [ + 'policy-versions' => $entry, + ]; + } + + /** + * 발행된 GDPR 정책 버전 1건을 멱등 생성합니다. + * + * @return GdprPolicyVersion 샘플 정책 버전 + */ + private function seedPolicyVersion(): GdprPolicyVersion + { + $actor = User::query()->orderBy('id')->first(); + + return GdprPolicyVersion::query()->firstOrCreate( + ['version' => self::SAMPLE_VERSION], + [ + 'change_type' => GdprPolicyChangeType::Initial, + 'memo' => 'API 문서 실측용 샘플 정책 버전', + 'snapshot' => [ + 'privacy_policy_slug' => 'privacy', + 'banner_enabled' => true, + 'banner_position' => 'bottom_bar', + ], + 'created_by' => $actor?->getKey(), + ] + ); + } +} diff --git a/plugins/_bundled/sirsoft-gdpr/tests/Unit/Support/ApiDocSampleServiceTest.php b/plugins/_bundled/sirsoft-gdpr/tests/Unit/Support/ApiDocSampleServiceTest.php new file mode 100644 index 00000000..0fc7c6f2 --- /dev/null +++ b/plugins/_bundled/sirsoft-gdpr/tests/Unit/Support/ApiDocSampleServiceTest.php @@ -0,0 +1,83 @@ +create([ + 'email' => 'apidoc-sample-user@example.com', + 'name' => 'API 문서 샘플 사용자', + ]); + } + + /** + * 시더가 계약을 구현한다. + */ + public function test_seeder_implements_contract(): void + { + $this->assertInstanceOf(ApiDocSampleSeeder::class, new ApiDocSampleService); + } + + /** + * 시더가 발행된 정책 버전 대표 샘플을 생성한다. + */ + public function test_seed_creates_policy_version(): void + { + (new ApiDocSampleService)->seed(); + + $version = GdprPolicyVersion::query()->where('version', 1)->first(); + + $this->assertNotNull($version); + $this->assertSame(1, $version->version); + $this->assertNotNull($version->change_type); + } + + /** + * 시더가 policy-versions 도메인의 path_params 맵을 반환한다. + */ + public function test_seed_returns_policy_versions_path_params_map(): void + { + $map = (new ApiDocSampleService)->seed(); + + $this->assertArrayHasKey('policy-versions', $map); + $entry = $map['policy-versions']; + $this->assertSame(GdprPolicyVersion::class, $entry['model']); + $this->assertSame('version', $entry['key']); + $this->assertArrayHasKey('version', $entry['path_params']); + $this->assertSame('1', $entry['path_params']['version']); + } + + /** + * 재실행 시 정책 버전이 중복 생성되지 않는다 (멱등). + */ + public function test_seed_is_idempotent(): void + { + (new ApiDocSampleService)->seed(); + (new ApiDocSampleService)->seed(); + + $this->assertSame(1, GdprPolicyVersion::query()->where('version', 1)->count()); + } +} diff --git a/plugins/_bundled/sirsoft-marketing/docs/api/channels.md b/plugins/_bundled/sirsoft-marketing/docs/api/channels.md index f2824076..ae949d04 100644 --- a/plugins/_bundled/sirsoft-marketing/docs/api/channels.md +++ b/plugins/_bundled/sirsoft-marketing/docs/api/channels.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +PUT /api/plugins/sirsoft-marketing/admin/channels HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) diff --git a/plugins/_bundled/sirsoft-marketing/docs/api/settings.md b/plugins/_bundled/sirsoft-marketing/docs/api/settings.md index 5134b7a3..9e45b5eb 100644 --- a/plugins/_bundled/sirsoft-marketing/docs/api/settings.md +++ b/plugins/_bundled/sirsoft-marketing/docs/api/settings.md @@ -27,6 +27,14 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/sirsoft-marketing/settings HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ diff --git a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/cbt.md b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/cbt.md index ee835e9d..7d1e5d9a 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/cbt.md +++ b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/cbt.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/sirsoft-pay_kginicis/admin/cbt-connectivity-check HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -38,6 +47,42 @@ _단건 응답: `data` 객체의 필드._ | hosts | array | `[{"name":"devcbt.inicis.com","env":"test","dns_resolved_i…` | 진단 대상 호스트별 결과 배열. 각 항목은 호스트명(`devcbt.inicis.com`), 환경(`test`), DNS 해석 IP(`dns_resolved_ip`), TCP 443 도달 여부(`tcp_443_reachable`)와 에러·응답지연(`tcp_443_error`, `tcp_443_latency_ms`)을 담는다. 운영계(`cbt.inicis.com`)는 화이트리스트 제약이 없어 제외된다. | | callback | object | `{"app_url":"https:\/\/test.example.com","callback_url":"h…` | 결제 콜백 URL 진단 정보. 앱 URL·콜백 URL과 각각의 HTTPS 여부(`app_url_https`, `callback_url_https`)·공인 호스트 여부(`app_url_public`, `callback_url_public`), 그리고 콜백 호스트가 앱 URL 호스트와 일치하는지(`host_matches_app_url`)를 담아 CBT 콜백 수신 가능 여부를 점검한다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-pay_kginicis::messages.cbt_connectivity.checked", + "data": { + "egress_ip": "59.10.38.149", + "server_ip": "127.0.0.1", + "hosts": [ + { + "name": "devcbt.inicis.com", + "env": "test", + "dns_resolved_ip": "183.109.71.154", + "tcp_443_reachable": false, + "tcp_443_error": "A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond", + "tcp_443_latency_ms": 3008 + } + ], + "callback": { + "app_url": "https://test.example.com", + "callback_url": "https://api.example.com/plugins/sirsoft-pay_kginicis/payment/cbt/callback", + "app_url_https": true, + "callback_url_https": true, + "app_url_public": true, + "callback_url_public": true, + "host_matches_app_url": false + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -62,9 +107,44 @@ _단건 응답: `data` 객체의 필드._ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_kginicis/admin/cbt-test-product HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| product_id | integer | `4` | product 식별자 (연관 리소스 참조) | +| product_code | string | `CBT-TEST-20260708063238` | 생성된 테스트 상품의 상품코드. `CBT-TEST-` 접두사에 생성 시각(YmdHis)을 붙여 자동 부여되며, SKU는 여기에 `KGINICIS-` 접두사를 더해 만들어진다. | +| admin_url | string | `/admin/ecommerce/products/4/edit` | admin URL | +| shop_url | string | `/shop/products/4?locale=ja` | shop URL | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "product_id": 4, + "product_code": "CBT-TEST-20260708063238", + "admin_url": "/admin/ecommerce/products/4/edit", + "shop_url": "/shop/products/4?locale=ja" + } +} +``` **에러 응답** diff --git a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/orders.md b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/orders.md index 9332bed0..e6bb1bf6 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/orders.md +++ b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/orders.md @@ -27,6 +27,15 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/sirsoft-pay_kginicis/admin/orders/test-mode-map HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -37,6 +46,22 @@ _단건 응답: `data` 객체의 필드._ | 20260619-1425382147 | boolean | `true` | 위와 동일 — 키가 주문번호, 값 `true`는 테스트 모드 결제 주문임을 나타낸다. | | APIDOC-KGINICIS-000001 | boolean | `true` | 위와 동일 — 키가 주문번호, 값 `true`는 테스트 모드 결제 주문임을 나타낸다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "APIDOC-KGINICIS-000001": true + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -62,10 +87,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_kginicis/admin/orders/APIDOC-KGINICIS-000001/cash-receipt HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -92,6 +130,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +**요청 예시** + +```http +GET /api/plugins/sirsoft-pay_kginicis/admin/orders/APIDOC-KGINICIS-000001/cbt-cvs HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -119,7 +166,7 @@ _단건 응답: `data` 객체의 필드._ | last_notify_result | string | `` | 마지막 입금 통보(NOTI) 처리 결과. `confirmed`(입금확정)·`ignored`(무시)·`failed`(검증실패) 중 하나가 기록된다. | | last_notify_reason | string | `` | 마지막 통보 처리 결과의 상세 사유(예: `deposit_confirmed`, `tid_mismatch`, `amount_mismatch`, `already_paid` 등). | | notify_history | array | `[]` | 최근 입금 통보 이력 목록(최대 10건). 각 항목은 수신시각·발신IP·결과·사유와 통보 payload 요약(tid·금액·통화 등)을 담는다. | -| notify_url | string | `https://g7_2.dev/plugins/sirsoft-pay_…` | notify URL | +| notify_url | string | `https://api.example.com/plugins/sirsoft-pay_…` | notify URL | | can_simulate_notify | boolean | `false` | simulate notify 수행 가능 여부 (권한 기반) | | can_mark_expired | boolean | `false` | mark expired 수행 가능 여부 (권한 기반) | | last_recheck_at | string | `` | last recheck 일시 | @@ -127,6 +174,49 @@ _단건 응답: `data` 객체의 필드._ | expired_at | string | `` | expired 일시 | | expiry_reason | string | `` | 만료 처리 사유. 입금 기한 경과로 만료된 경우 `payment_term_elapsed` 가 기록된다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "is_cbt_cvs": true, + "order_number": "APIDOC-KGINICIS-000001", + "order_status": "pending_payment", + "payment_status": "paid", + "tid": "INIJPGCARDapidocsmpl0000000001", + "amount": 5000, + "currency": "KRW", + "cbt_mid": "apidocmid1", + "cbt_sid": "apidocsid1", + "is_test_mode": true, + "convenience": "seven_eleven", + "conf_no": "1234567890", + "receipt_no": "0987654321", + "payment_term": "20260710235959", + "payment_term_formatted": "2026-07-10 23:59:59", + "is_expired_by_time": false, + "cvs_status": "waiting_deposit", + "last_notify_at": "", + "last_notify_result": "", + "last_notify_reason": "", + "notify_history": [], + "notify_url": "https://api.example.com/plugins/sirsoft-pay_kginicis/payment/cbt/cvs-notify", + "can_simulate_notify": false, + "can_mark_expired": false, + "last_recheck_at": "", + "last_recheck_result": "", + "expired_at": "", + "expiry_reason": "" + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -153,10 +243,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_kginicis/admin/orders/APIDOC-KGINICIS-000001/cbt-cvs/expire HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -183,9 +286,92 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_kginicis/admin/orders/APIDOC-KGINICIS-000001/cbt-cvs/recheck HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| is_cbt_cvs | boolean | `true` | cbt cvs 여부 | +| order_number | string | `APIDOC-KGINICIS-000001` | 주문번호 | +| order_status | string | `pending_payment` | 주문상태 (OrderStatusEnum) | +| payment_status | string | `paid` | 결제 상태값(PaymentStatusEnum). CVS 입금 흐름에서는 `waiting_deposit`(입금대기) → `paid`(입금완료) 또는 `expired`(기한만료) 로 전이한다. | +| tid | string | `INIJPGCARDapidocsmpl0000000001` | KG 이니시스 거래 ID(transaction_id). 결제 승인·통보를 식별하는 이니시스 측 거래번호다. | +| amount | integer | `5000` | 청구 금액. 결제 메타의 `cvs_amount`(CVS 통보 기준 결제 통화 환산액)를 우선 사용하고, 없으면 결제 승인액 또는 주문 총 청구액으로 대체한다. | +| currency | string | `KRW` | 결제 통화 (KRW, USD, EUR 등) | +| cbt_mid | string | `apidocmid1` | CBT(일본결제)에 사용된 KG 이니시스 일본 가맹점 ID. 입금 통보(NOTI) 검증 시 통보의 mid와 대조하는 기준값이다. | +| cbt_sid | string | `apidocsid1` | CBT 결제 세션/상점 식별자(SID). 입금 통보의 sid와 일치하는지 검증하는 데 사용된다. | +| is_test_mode | boolean | `true` | test mode 여부 | +| convenience | string | `seven_eleven` | 구매자가 선택한 일본 편의점 식별값. 어느 편의점에서 입금하는지를 나타낸다. | +| conf_no | string | `1234567890` | 편의점 입금용 확인번호(수납확인번호). 구매자가 편의점 단말에서 입력·제시하는 번호다. | +| receipt_no | string | `0987654321` | 편의점 입금용 접수번호(수납번호). 확인번호와 함께 편의점 결제 접수를 식별한다. | +| payment_term | string | `20260710235959` | 편의점 입금 마감 일시(YmdHis 압축 문자열). 이 기한이 지나면 시간 경과 만료 대상이 된다. | +| payment_term_formatted | string | `2026-07-10 23:59:59` | `payment_term` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| is_expired_by_time | boolean | `false` | expired by time 여부 | +| cvs_status | string | `waiting_deposit` | CVS 입금 상태(`payment_meta.cvs_status`). 입금대기(`waiting_deposit`)·입금완료(`paid`)·만료(`expired`) 등을 나타내며, 메타값이 없고 결제가 입금대기면 `waiting_deposit`로 채운다. | +| last_notify_at | string | `` | last notify 일시 | +| last_notify_result | string | `` | 마지막 입금 통보(NOTI) 처리 결과. `confirmed`(입금확정)·`ignored`(무시)·`failed`(검증실패) 중 하나가 기록된다. | +| last_notify_reason | string | `` | 마지막 통보 처리 결과의 상세 사유(예: `deposit_confirmed`, `tid_mismatch`, `amount_mismatch`, `already_paid` 등). | +| notify_history | array | `[]` | 최근 입금 통보 이력 목록(최대 10건). 각 항목은 수신시각·발신IP·결과·사유와 통보 payload 요약(tid·금액·통화 등)을 담는다. | +| notify_url | string | `http://localhost/plugins/sirsoft-pay_…` | notify URL | +| can_simulate_notify | boolean | `false` | simulate notify 수행 가능 여부 (권한 기반) | +| can_mark_expired | boolean | `false` | mark expired 수행 가능 여부 (권한 기반) | +| last_recheck_at | string | `2026-07-08T06:32:39+00:00` | last recheck 일시 | +| last_recheck_result | string | `local_status_checked` | 마지막 로컬 상태 재확인 결과. recheck 액션 실행 시 `local_status_checked` 로 기록되며, CBT 편의점 입금은 외부 PG 조회 대상이 아니라 로컬 확인 흔적만 남긴다. | +| expired_at | string | `` | expired 일시 | +| expiry_reason | string | `` | 만료 처리 사유. 입금 기한 경과로 만료된 경우 `payment_term_elapsed` 가 기록된다. | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "sirsoft-pay_kginicis::messages.cbt_cvs.recheck_success", + "data": { + "is_cbt_cvs": true, + "order_number": "APIDOC-KGINICIS-000001", + "order_status": "pending_payment", + "payment_status": "paid", + "tid": "INIJPGCARDapidocsmpl0000000001", + "amount": 5000, + "currency": "KRW", + "cbt_mid": "apidocmid1", + "cbt_sid": "apidocsid1", + "is_test_mode": true, + "convenience": "seven_eleven", + "conf_no": "1234567890", + "receipt_no": "0987654321", + "payment_term": "20260710235959", + "payment_term_formatted": "2026-07-10 23:59:59", + "is_expired_by_time": false, + "cvs_status": "waiting_deposit", + "last_notify_at": "", + "last_notify_result": "", + "last_notify_reason": "", + "notify_history": [], + "notify_url": "http://localhost/plugins/sirsoft-pay_kginicis/payment/cbt/cvs-notify", + "can_simulate_notify": false, + "can_mark_expired": false, + "last_recheck_at": "2026-07-08T06:32:39+00:00", + "last_recheck_result": "local_status_checked", + "expired_at": "", + "expiry_reason": "" + } +} +``` **에러 응답** @@ -213,10 +399,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_kginicis/admin/orders/APIDOC-KGINICIS-000001/cbt-cvs/simulate-notify HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -243,12 +442,35 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +**요청 예시** + +```http +GET /api/plugins/sirsoft-pay_kginicis/admin/orders/APIDOC-KGINICIS-000001/cbt-reconciliation HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": null +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -275,10 +497,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_kginicis/admin/orders/APIDOC-KGINICIS-000001/cbt-reconciliation/refund-retry HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -305,6 +540,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +**요청 예시** + +```http +GET /api/plugins/sirsoft-pay_kginicis/admin/orders/APIDOC-KGINICIS-000001/escrow-delivery HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -320,6 +564,49 @@ _단건 응답: `data` 객체의 필드._ | escrow_confirm | null | `null` | 구매확정 이력(`payment_meta.escrow_confirm`). 구매자 구매확정 처리 전이면 `null`이다. | | deny_confirmed | boolean | `false` | 판매자 구매거절 확인 이력(`payment_meta.escrow_deny_confirm`) 존재 여부. 이미 거절확인했으면 `true`가 되어 중복 처리를 막는다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "has_escrow_payment": true, + "tid": "INIJPGCARDapidocsmpl0000000001", + "price": 460829, + "courier_codes": { + "hanjin": "한진택배", + "cjgls": "CJ대한통운", + "loge": "롯데택배", + "epost": "우체국택배", + "lotte": "롯데글로벌로지스", + "kdexp": "경동택배", + "cvs": "편의점택배", + "ilyang": "일양로지스", + "chunil": "천일택배", + "cvsnet": "CVSnet편의점", + "daesin": "대신택배", + "kunyoung": "건영택배", + "gsilogis": "GSI Express", + "etc": "기타" + }, + "prefill": { + "recvName": "API 문서 샘플 수령인", + "recvTel": "010-0000-0002", + "recvPost": "06134", + "recvAddr": "서울특별시 강남구 테헤란로 001 API 문서 샘플 빌딩 1층" + }, + "registered_delivery": null, + "escrow_confirm": null, + "deny_confirmed": false + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -346,10 +633,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_kginicis/admin/orders/APIDOC-KGINICIS-000001/escrow-delivery HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -376,10 +676,23 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_kginicis/admin/orders/APIDOC-KGINICIS-000001/escrow-deny-confirm HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -406,6 +719,15 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +**요청 예시** + +```http +GET /api/plugins/sirsoft-pay_kginicis/admin/orders/APIDOC-KGINICIS-000001/transaction-status HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ @@ -459,6 +781,77 @@ _단건 응답: `data` 객체의 필드._ | _local_notice | string | `CBT 거래는 한국 INIAPI 거래조회 대상이 아니므로 저장된 승…` | CBT 로컬 확인 경로에서만 채워지는 안내 문구. 이 거래가 실시간 PG 조회가 아니라 저장된 승인/입금 정보로 표시됨을 관리자에게 알린다. | | _cbt_cvs | object | `{"status":"waiting_deposit","last_notify_at":"","last_not…` | CBT 편의점(CVS) 결제일 때만 채워지는 입금 운영 요약. 입금 상태·최근 통보/재확인 결과·만료 정보·최근 통보 이력(최대 10건)을 담으며, CVS가 아니면 `null`이다. | +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "messages.success", + "data": { + "resultCode": "0000", + "resultMsg": "정상처리", + "tid": "INIJPGCARDapidocsmpl0000000001", + "_is_cbt": true, + "_is_local_confirmation": true, + "_is_test_mode": true, + "_local_is_escrow": true, + "_pay_method": "CVS", + "_base_pay_method_label": "일본 편의점결제", + "_embedded_pg_provider": null, + "_embedded_pg_provider_label": null, + "_pay_method_label": "일본 편의점결제", + "_auth_code": null, + "_auth_date": "2026-07-08 06:32:34", + "_total_price": "5000", + "_currency": "JPY", + "_moid": "APIDOC-KGINICIS-000001", + "_buyer_name": "API 문서 샘플 구매자", + "_buyer_email": "apidoc-sample-user@example.com", + "_buyer_tel": "010-0000-0001", + "_status": "waiting_deposit", + "_cancel_price": null, + "_cancel_date": null, + "_part_cancel_list": [], + "_card_name": "신한카드", + "_card_num": "2570-****-****-6458", + "_card_code": null, + "_card_quota": "일시불", + "_card_interest": null, + "_vbank_num": null, + "_vbank_bank_code": null, + "_vbank_bank_name": "CVS", + "_vbank_holder": null, + "_vbank_expire_date": "2026-07-10 23:59:59", + "_vbank_status": "waiting_deposit", + "_vbank_paid_at": null, + "_bank_code": null, + "_bank_name": null, + "_bank_acnt_num": null, + "_hpp_num": null, + "_hpp_corp": null, + "_escrow_status": null, + "_escrow_confirm": null, + "_inquiry_at": "2026-07-08 06:32:40", + "_local_notice": "CBT 거래는 한국 INIAPI 거래조회 대상이 아니므로 저장된 승인/입금 확인 정보로 표시됩니다.", + "_cbt_cvs": { + "status": "waiting_deposit", + "last_notify_at": "", + "last_notify_result": "", + "last_notify_reason": "", + "last_recheck_at": "", + "last_recheck_result": "", + "expired_at": "", + "expiry_reason": "", + "notify_history": [] + } + } +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -485,12 +878,84 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +**요청 예시** + +```http +GET /api/plugins/sirsoft-pay_kginicis/user/orders/APIDOC-KGINICIS-000001/receipt HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능) +``` + **응답 필드** (`data` 내부) +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "receipt_type": "cbt_confirmation", + "receipt_url": null, + "receipt_label": "결제확인", + "receipt_view_label": "결제확인서 보기", + "receipt_title": "KG 이니시스 CBT 결제확인서", + "receipt_notice": "일본 CBT 결제는 한국 KG 이니시스 매출전표 조회와 별도로 결제 승인 정보를 표시합니다.", + "receipt_fields": [ + { + "label": "주문번호", + "value": "APIDOC-KGINICIS-000001" + }, + { + "label": "결제수단", + "value": "일본 편의점결제" + }, + { + "label": "거래번호", + "value": "INIJPGCARDapidocsmpl0000000001" + }, + { + "label": "결제금액", + "value": "5,000 KRW" + }, + { + "label": "입금 상태", + "value": "입금완료" + }, + { + "label": "편의점 코드", + "value": "seven_eleven" + }, + { + "label": "편의점 확인번호", + "value": "1234567890" + }, + { + "label": "편의점 접수번호", + "value": "0987654321" + }, + { + "label": "입금 마감일시", + "value": "2026-07-10 23:59:59" + } + ], + "is_test_mode": true, + "payment_method_label": "일본 편의점결제", + "payment_method_display_label": "일본 편의점결제", + "cbt_pay_method": "CVS", + "payment_status": "paid", + "selected_payment_method": null, + "embedded_pg_provider": null, + "embedded_pg_provider_label": null +} +``` + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/payment.md b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/payment.md index d79c0c52..5f6c0863 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/payment.md +++ b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/payment.md @@ -27,10 +27,22 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_kginicis/payment/cbt/checkout-token HTTP/1.1 +Host: api.example.com +Accept: application/json +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** _대표 에러 없음 (공개 조회). _ @@ -52,10 +64,22 @@ _대표 에러 없음 (공개 조회). +**응답 예시** + + + **에러 응답** _대표 에러 없음 (공개 조회). _ @@ -84,10 +108,32 @@ _대표 에러 없음 (공개 조회). +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -117,10 +163,31 @@ PC 표준결제창(KRW)에서 사용자가 결제를 완료하지 않고 창을 | buyer_email | body | string | 아니오 | max 255 | 구매자 이메일. 제공 시 주문의 구매자 정보와 대조해 본인 결제 요청인지 확인하는 데 사용된다. | | buyer_phone | body | string | 아니오 | max 30 | 구매자 전화번호. 제공 시 주문의 구매자 정보와 대조해 본인 결제 요청인지 확인하는 데 사용된다. | +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_kginicis/payment/mobile/signature HTTP/1.1 +Host: api.example.com +Accept: application/json +Content-Type: application/json + +{ + "oid": "예시값", + "price": 1, + "timestamp": "예시값", + "buyer_email": "user@example.com", + "buyer_phone": "010-1234-5678" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | @@ -150,10 +217,31 @@ PC 표준결제창(KRW)에서 사용자가 결제를 완료하지 않고 창을 | buyer_email | body | string | 아니오 | max 255 | 구매자 이메일. 제공 시 주문의 구매자 정보와 대조해 본인 결제 요청인지 확인하는 데 사용된다. | | buyer_phone | body | string | 아니오 | max 30 | 구매자 전화번호. 제공 시 주문의 구매자 정보와 대조해 본인 결제 요청인지 확인하는 데 사용된다. | +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_kginicis/payment/signature HTTP/1.1 +Host: api.example.com +Accept: application/json +Content-Type: application/json + +{ + "oid": "예시값", + "price": 1, + "timestamp": "예시값", + "buyer_email": "user@example.com", + "buyer_phone": "010-1234-5678" +} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/transaction.md b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/transaction.md index 0e32c318..94b2273e 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/transaction.md +++ b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/transaction.md @@ -27,10 +27,23 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_kginicis/admin/transaction/query HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) +**응답 예시** + + + **에러 응답** | 상태코드 | 의미 | 발생 조건 | diff --git a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/vbank.md b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/vbank.md index 10e19d51..4ed9381e 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/vbank.md +++ b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/vbank.md @@ -26,14 +26,39 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/sirsoft-pay_kginicis/admin/vbank-notify-url HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) _단건 응답: `data` 객체의 필드._ | 필드 | 타입 | 실측 예시값 | 용도/설명 | | --- | --- | --- | --- | -| url | string | `https://g7_2.dev/plugins/sirsoft-pay_…` | PC 웹용 가상계좌 입금통보(NOTI) 수신 콜백 URL(`/plugins/sirsoft-pay_kginicis/payment/vbank-notify`)의 절대 주소. 운영자가 이 값을 KG 이니시스 가맹점 설정에 입금통보 URL로 등록하면, 구매자가 가상계좌에 실제 입금했을 때 KG 이니시스가 이 주소로 통보한다. | -| mobile_url | string | `https://g7_2.dev/plugins/sirsoft-pay_…` | mobile URL | +| url | string | `https://api.example.com/plugins/sirsoft-pay_…` | PC 웹용 가상계좌 입금통보(NOTI) 수신 콜백 URL(`/plugins/sirsoft-pay_kginicis/payment/vbank-notify`)의 절대 주소. 운영자가 이 값을 KG 이니시스 가맹점 설정에 입금통보 URL로 등록하면, 구매자가 가상계좌에 실제 입금했을 때 KG 이니시스가 이 주소로 통보한다. | +| mobile_url | string | `https://api.example.com/plugins/sirsoft-pay_…` | mobile URL | + +**응답 예시** + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "data": { + "url": "https://api.example.com/plugins/sirsoft-pay_kginicis/payment/vbank-notify", + "mobile_url": "https://api.example.com/plugins/sirsoft-pay_kginicis/payment/mobile/vbank-notify" + } +} +``` **에러 응답** diff --git a/plugins/_bundled/sirsoft-verification_kginicis/docs/api/identity.md b/plugins/_bundled/sirsoft-verification_kginicis/docs/api/identity.md index 72ef97d5..f8e430e0 100644 --- a/plugins/_bundled/sirsoft-verification_kginicis/docs/api/identity.md +++ b/plugins/_bundled/sirsoft-verification_kginicis/docs/api/identity.md @@ -27,9 +27,20 @@ _요청 파라미터 없음._ +**요청 예시** + +```http +GET /api/plugins/sirsoft-verification_kginicis/me/identity/inicis HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + **응답 필드** (`data` 내부) - + + + **에러 응답** @@ -59,7 +70,7 @@ _요청 파라미터 없음._ | is_foreigner | boolean | `false` | 외국인 여부. | 이 외에 `BaseApiResource` 공통 메타 `is_owner`(항상 본인이므로 `true`) + `abilities` 가 함께 붙는다. -- **미설치 주의**: 이 문서는 플러그인 미설치 상태의 라우트 정적 분석으로 생성되어 실측 응답이 없다(`no-token`). 설치 후 `php artisan api:docgen --scope=plugin:sirsoft-verification_kginicis --seed` 로 실측하면 응답 예시가 채워진다. +- **응답 예시 주의**: 실측 시 샘플 사용자에게 본인인증 record 가 없어 `data: null`(record 없음) 응답만 관측된다. 아래 두 응답 예시 중 "record 존재 시" 는 `InicisIdentityResource` 구조 기준 정적 작성이다. 실제 record 가 있는 사용자로 실측하면 마스킹된 본인확인 정보가 채워진다. **응답 예시** (record 존재 시 — 정적, `InicisIdentityResource` 구조 기준) diff --git a/tests/Feature/Console/ApiDocgenCommandTest.php b/tests/Feature/Console/ApiDocgenCommandTest.php new file mode 100644 index 00000000..4c10a8f4 --- /dev/null +++ b/tests/Feature/Console/ApiDocgenCommandTest.php @@ -0,0 +1,164 @@ +collect('core'); + + $this->assertNotEmpty($routes, '코어 API 라우트가 수집되어야 한다.'); + // 대표 코어 라우트(사용자 목록)가 포함되는지 (전수 수집 실증). + $uris = array_column($routes, 'uri'); + $this->assertContains('/api/admin/users', $uris); + + // --dry-run 실행이 수집 개수를 보고하고 생성 없이 종료한다. + $this->artisan('api:docgen', ['--scope' => 'core', '--dry-run' => true]) + ->expectsOutputToContain('라우트 수집 (scope=core)') + ->assertSuccessful(); + } + + /** + * (b) 확장 소유 라우트는 확장 디렉토리 파일로 라우팅됩니다. + */ + #[Test] + public function 확장_라우트는_확장_디렉토리_파일로_라우팅된다(): void + { + $inventory = app(ApiRouteInventory::class); + + // 코어 라우트의 소유는 core, 출력 파일은 docs/backend/api 하위여야 한다. + $coreRoutes = $inventory->collect('core'); + foreach ($coreRoutes as $route) { + $this->assertSame('core', $route['owner']['type']); + } + + // 번들 확장 라우트가 있으면 그 소유 type 이 module/plugin 이고 id 가 있어야 한다 + // (targetFile 이 이 owner 로 modules|plugins/_bundled/{id}/docs/api 경로를 만든다). + $all = $inventory->collect('all'); + $extRoutes = array_filter($all, fn ($r) => $r['owner']['type'] !== 'core'); + + if ($extRoutes === []) { + // 이 환경에 활성 확장이 없으면(라우트 0) 스킵 — core 라우팅 검증만으로 충분. + $this->markTestSkipped('활성 확장 라우트 없음 — 확장 라우팅 검증 스킵.'); + } + + foreach ($extRoutes as $route) { + $this->assertContains($route['owner']['type'], ['module', 'plugin']); + $this->assertNotEmpty($route['owner']['id']); + } + } + + /** + * (c) 컨트롤러 메서드의 FormRequest rules() 를 정적 리플렉션으로 추출합니다. + */ + #[Test] + public function form_request_rules를_정적_추출한다(): void + { + $introspector = app(FormRequestIntrospector::class); + + // 로그인 컨트롤러는 email/password rules 를 가진 FormRequest 를 받는다. + $result = $introspector->introspect( + AuthController::class, + 'login' + ); + + $names = array_column($result['params'], 'name'); + $this->assertContains('email', $names, 'FormRequest rules 의 email 파라미터가 추출되어야 한다.'); + $this->assertContains('password', $names); + } + + /** + * (d) 재생성해도 사람이 채운 서술은 보존되고 추출 블록만 갱신됩니다(멱등). + */ + #[Test] + public function 재생성은_사람_서술을_보존하는_멱등_동작이다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', 'uri' => '/api/me', 'name' => 'api.me.show', + 'controller' => 'C', 'controller_method' => 'show', 'permission' => null, + 'middleware' => ['auth:sanctum'], 'path_params' => [], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['success', 'data'], 'shape' => 'object', 'fields' => [['name' => 'id', 'type' => 'integer', 'sample' => '1']], 'pagination' => false]; + $probeMeta = ['status' => 200, 'skipped_reason' => null, 'base_url' => 'https://api.example.com', 'resolved_uri' => '/api/me', 'body' => ['success' => true, 'data' => ['id' => 1], 'message' => null, 'error' => null]]; + + $section = $scaffolder->endpointSection($route, $request, $schema, $probeMeta); + $header = "# Me\n"; + $first = $scaffolder->mergeDocument(null, $header, [$section], ['api.me.show']); + + // @generated 블록 밖 `**설명**` 슬롯의 TODO 스텁을 사람 서술로 채운다 + // (extractHumanProse 는 기본 TODO 스텁이 남아 있으면 보존 대상 없음으로 판정). + $human = str_replace( + '**설명** ', + '**설명** 이 엔드포인트는 현재 로그인 사용자를 반환합니다. (사람 서술)', + $first + ); + + // 재생성 시 사람 서술은 보존되고 생성 블록만 갱신된다. + $second = $scaffolder->mergeDocument($human, $header, [$section], ['api.me.show']); + + $this->assertStringContainsString('이 엔드포인트는 현재 로그인 사용자를 반환합니다. (사람 서술)', $second); + $this->assertStringNotContainsString('TODO: 이 엔드포인트의 용도', $second); + // 다시 병합해도 사람 서술 중복 없이 멱등. + $third = $scaffolder->mergeDocument($second, $header, [$section], ['api.me.show']); + $this->assertSame( + substr_count($second, '(사람 서술)'), + substr_count($third, '(사람 서술)'), + '재병합 시 사람 서술이 중복되지 않아야 한다.' + ); + } + + /** + * (e) --check 는 대응 문서가 없으면 drift 로 실패 코드를 반환합니다. + */ + #[Test] + public function check는_문서_누락을_drift로_검출한다(): void + { + // 존재하지 않는 확장 스코프(문서 파일 부재 보장)로 --check 실행. + // 라우트가 0이면 커맨드는 성공으로 조기 종료하므로, 라우트가 있는 core 로 검증한다. + // core 문서는 이미 존재하므로(35파일), 임시로 한 파일을 백업/삭제해 누락을 유발한다. + $target = base_path('docs/backend/api/me.md'); + $backup = null; + if (File::exists($target)) { + $backup = File::get($target); + File::delete($target); + } + + try { + // me.md 가 없으면 --check 는 "문서 파일 없음" drift 로 FAILURE 를 반환한다. + $this->artisan('api:docgen', ['--scope' => 'core', '--check' => true]) + ->assertFailed(); + } finally { + if ($backup !== null) { + File::put($target, $backup); + } + } + } +} diff --git a/tests/Feature/Console/ApiEndpointProbeWriteTest.php b/tests/Feature/Console/ApiEndpointProbeWriteTest.php new file mode 100644 index 00000000..d3ad392b --- /dev/null +++ b/tests/Feature/Console/ApiEndpointProbeWriteTest.php @@ -0,0 +1,110 @@ +seed(); + $userId = User::where($map['users']['key'], $map['users']['value'])->value('id'); + + $probe = new ApiEndpointProbe(config('app.url')); + $probe->authenticate($userId); + + return [$probe, User::find($userId)]; + } + + #[Test] + public function 쓰기_메서드는_트랜잭션_롤백으로_레코드를_영속시키지_않는다(): void + { + [$probe] = $this->preparedProbe(); + + $before = Role::count(); + + $result = $probe->probe('POST', '/api/admin/roles', [ + 'identifier' => 'probe_write_'.substr(uniqid(), 0, 8), + 'name' => '실측 예시값', + 'is_active' => true, + ]); + + // 응답은 관측되지만(성공 2xx) 레코드는 롤백되어 남지 않는다. + $this->assertNotNull($result['status']); + $this->assertSame($before, Role::count(), '쓰기 실측 후 레코드가 롤백되지 않고 남았습니다'); + + $probe->cleanup(); + } + + #[Test] + public function 성공한_쓰기_실측은_envelope_body_를_반환한다(): void + { + [$probe] = $this->preparedProbe(); + + $result = $probe->probe('POST', '/api/admin/roles', [ + 'identifier' => 'probe_env_'.substr(uniqid(), 0, 8), + 'name' => '실측 예시값', + 'is_active' => true, + ]); + + if ($result['ok']) { + $this->assertIsArray($result['body']); + $this->assertArrayHasKey('success', $result['body']); + $this->assertArrayHasKey('data', $result['body']); + } else { + // 검증 규칙이 도메인 특이해 실패할 수 있으나, 그 경우에도 skipped_reason 이 명확해야 한다. + $this->assertNotNull($result['skipped_reason']); + } + + $probe->cleanup(); + } + + #[Test] + public function 연속_쓰기_실측이_서로_인증_컨텍스트를_오염시키지_않는다(): void + { + [$probe] = $this->preparedProbe(); + + // 연속 쓰기 실측(각각 guard 조작 후 forgetGuards)이 서로 오염되지 않아야 한다. + $first = $probe->probe('POST', '/api/admin/roles', ['identifier' => 'probe_a_'.uniqid(), 'name' => 'a', 'is_active' => true]); + $second = $probe->probe('POST', '/api/admin/roles', ['identifier' => 'probe_b_'.uniqid(), 'name' => 'b', 'is_active' => true]); + + // 두 번째 실측이 첫 번째의 인증 컨텍스트 오염으로 401 이 되지 않아야 한다. + $this->assertNotSame(401, $first['status']); + $this->assertNotSame(401, $second['status']); + + $probe->cleanup(); + } + + #[Test] + public function unresolved_path_는_실측하지_않고_사유를_남긴다(): void + { + [$probe] = $this->preparedProbe(); + + $result = $probe->probe('PUT', '/api/admin/roles/{role}', ['name' => 'x']); + + $this->assertFalse($result['ok']); + $this->assertSame('unresolved-path-param', $result['skipped_reason']); + + $probe->cleanup(); + } +} diff --git a/tests/Unit/Support/ApiDoc/ApiDocPipelineTest.php b/tests/Unit/Support/ApiDoc/ApiDocPipelineTest.php index cb4d1c00..0e370730 100644 --- a/tests/Unit/Support/ApiDoc/ApiDocPipelineTest.php +++ b/tests/Unit/Support/ApiDoc/ApiDocPipelineTest.php @@ -309,4 +309,535 @@ class ApiDocPipelineTest extends TestCase $this->assertStringContainsString('실제 사람이 작성한 설명입니다.', $regenerated); $this->assertStringNotContainsString('TODO: 이 엔드포인트의 용도', $regenerated); } + + #[Test] + public function ge_t_인증_엔드포인트_요청_예시가_raw_htt_p_요청이다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', 'uri' => '/api/me', 'name' => 'api.me.show', + 'controller' => 'C', 'controller_method' => 'show', 'permission' => null, + 'middleware' => ['auth:sanctum'], 'path_params' => [], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['data'], 'shape' => 'object', 'fields' => [['name' => 'id', 'type' => 'integer', 'sample' => '1']], 'pagination' => false]; + $probeMeta = [ + 'status' => 200, 'skipped_reason' => null, + 'base_url' => 'https://g7.example.com', 'resolved_uri' => '/api/me', + 'body' => ['success' => true, 'data' => ['id' => 1], 'message' => null, 'error' => null], + ]; + + $section = $scaffolder->endpointSection($route, $request, $schema, $probeMeta); + + $this->assertStringContainsString('**요청 예시**', $section); + // curl 이 아니라 raw HTTP 요청 라인 + 헤더 + $this->assertStringNotContainsString('curl', $section); + $this->assertStringContainsString('GET /api/me HTTP/1.1', $section); + // Host 는 실측 기준 URL(g7.example.com)이 아니라 공개 placeholder 로 마스킹된다. + $this->assertStringContainsString('Host: api.example.com', $section); + $this->assertStringNotContainsString('g7.example.com', $section); + $this->assertStringContainsString('Accept: application/json', $section); + $this->assertStringContainsString('Authorization: Bearer {YOUR_TOKEN}', $section); + // 실측 토큰 평문이 유출되지 않아야 한다 (placeholder 마스킹) + $this->assertStringNotContainsString('Bearer eyJ', $section); + } + + #[Test] + public function 바디_메서드_요청_예시가_json_바디를_가진_raw_htt_p_요청이다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'POST', 'uri' => '/api/admin/users', 'name' => 'api.admin.users.store', + 'controller' => 'C', 'controller_method' => 'store', 'permission' => 'core.users.create', + 'middleware' => ['auth:sanctum'], 'path_params' => [], + ]; + $request = ['request_class' => 'X', 'params' => [ + ['name' => 'name', 'type' => 'string', 'required' => true, 'allowed' => 'max 255'], + ['name' => 'age', 'type' => 'integer', 'required' => true, 'allowed' => ''], + ['name' => 'nickname', 'type' => 'string', 'required' => false, 'allowed' => ''], + ], 'hook_filters' => []]; + // 쓰기 메서드는 실측 제외 → body null + $probeMeta = [ + 'status' => null, 'skipped_reason' => 'write-method', + 'base_url' => 'https://g7.example.com', 'resolved_uri' => '/api/admin/users', 'body' => null, + ]; + + $section = $scaffolder->endpointSection($route, $request, null, $probeMeta); + + // curl 이 아니라 raw HTTP 요청 라인 + Content-Type 헤더 + JSON 바디 + $this->assertStringNotContainsString('curl', $section); + $this->assertStringContainsString('POST /api/admin/users HTTP/1.1', $section); + $this->assertStringContainsString('Host: api.example.com', $section); + $this->assertStringContainsString('Content-Type: application/json', $section); + // 전체 파라미터를 바디로 조립하고, 이름 기반 현실적 값을 채운다(placeholder "string" 남발 방지). + $this->assertStringContainsString('"name": "예시 이름"', $section); + $this->assertStringContainsString('"age": 1', $section); + $this->assertStringContainsString('"nickname"', $section); + // 응답 예시는 실측 제외 마커 + $this->assertStringContainsString('실측 제외: write-method — 응답 예시는 사람이 작성', $section); + } + + #[Test] + public function 실측_body_응답_예시는_envelope_통짜_json_이다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', 'uri' => '/api/me', 'name' => 'api.me.show', + 'controller' => 'C', 'controller_method' => 'show', 'permission' => null, + 'middleware' => ['auth:sanctum'], 'path_params' => [], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['success', 'data'], 'shape' => 'object', 'fields' => [['name' => 'id', 'type' => 'integer', 'sample' => '1']], 'pagination' => false]; + $body = ['success' => true, 'data' => ['id' => 1, 'name' => '홍길동'], 'message' => null, 'error' => null]; + $probeMeta = ['status' => 200, 'skipped_reason' => null, 'base_url' => 'https://g7.example.com', 'resolved_uri' => '/api/me', 'body' => $body]; + + $section = $scaffolder->endpointSection($route, $request, $schema, $probeMeta); + + $this->assertStringContainsString('**응답 예시**', $section); + $this->assertStringContainsString('HTTP/1.1 200', $section); + $this->assertStringContainsString('"success": true', $section); + $this->assertStringContainsString('"홍길동"', $section); + $this->assertStringContainsString('"message": null', $section); + } + + #[Test] + public function 목록_응답_예시는_2항목으로_절단된다(): void + { + $scaffolder = new ApiDocScaffolder; + + $rows = []; + for ($i = 1; $i <= 5; $i++) { + $rows[] = ['id' => $i, 'name' => "user{$i}"]; + } + $body = [ + 'success' => true, + 'data' => ['data' => $rows, 'pagination' => ['total' => 5, 'current_page' => 1]], + 'message' => null, 'error' => null, + ]; + + $route = [ + 'method' => 'GET', 'uri' => '/api/admin/users', 'name' => 'api.admin.users.index', + 'controller' => 'C', 'controller_method' => 'index', 'permission' => null, + 'middleware' => ['auth:sanctum'], 'path_params' => [], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['success', 'data'], 'shape' => 'collection', 'fields' => [['name' => 'id', 'type' => 'integer', 'sample' => '1']], 'pagination' => true]; + $probeMeta = ['status' => 200, 'skipped_reason' => null, 'base_url' => 'https://g7.example.com', 'resolved_uri' => '/api/admin/users', 'body' => $body]; + + $section = $scaffolder->endpointSection($route, $request, $schema, $probeMeta); + + // 대표 2항목 + 절단 주석 항목. user3~5 는 나타나지 않아야 한다. + $this->assertStringContainsString('"user1"', $section); + $this->assertStringContainsString('"user2"', $section); + $this->assertStringNotContainsString('"user3"', $section); + $this->assertStringContainsString('총 5건 중 2건 표시', $section); + } + + #[Test] + public function 요청_응답_예시_블록은_generated_블록_내부에_있어_재생성_대상이다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', 'uri' => '/api/me', 'name' => 'api.me.show', + 'controller' => 'C', 'controller_method' => 'show', 'permission' => null, + 'middleware' => ['auth:sanctum'], 'path_params' => [], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['success', 'data'], 'shape' => 'object', 'fields' => [['name' => 'id', 'type' => 'integer', 'sample' => '1']], 'pagination' => false]; + $body = ['success' => true, 'data' => ['id' => 1], 'message' => null, 'error' => null]; + $probeMeta = ['status' => 200, 'skipped_reason' => null, 'base_url' => 'https://g7.example.com', 'resolved_uri' => '/api/me', 'body' => $body]; + + $section = $scaffolder->endpointSection($route, $request, $schema, $probeMeta); + + $genStart = strpos($section, '@generated:start'); + $genEnd = strpos($section, '@generated:end'); + $reqExample = strpos($section, '**요청 예시**'); + $respExample = strpos($section, '**응답 예시**'); + + // 요청/응답 예시 블록은 모두 @generated 경계 내부(재생성 대상) + $this->assertGreaterThan($genStart, $reqExample); + $this->assertLessThan($genEnd, $reqExample); + $this->assertGreaterThan($genStart, $respExample); + $this->assertLessThan($genEnd, $respExample); + } + + #[Test] + public function 요청_예시가_generated_블록_안이라_재생성해도_멱등이다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', 'uri' => '/api/me', 'name' => 'api.me.show', + 'controller' => 'C', 'controller_method' => 'show', 'permission' => null, + 'middleware' => ['auth:sanctum'], 'path_params' => [], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['success', 'data'], 'shape' => 'object', 'fields' => [['name' => 'id', 'type' => 'integer', 'sample' => '1']], 'pagination' => false]; + $body = ['success' => true, 'data' => ['id' => 1], 'message' => null, 'error' => null]; + $probeMeta = ['status' => 200, 'skipped_reason' => null, 'base_url' => 'https://g7.example.com', 'resolved_uri' => '/api/me', 'body' => $body]; + + $section = $scaffolder->endpointSection($route, $request, $schema, $probeMeta); + $header = "# Me\n"; + + // 사람 서술을 채운 뒤 재생성해도 예시(생성 블록)는 갱신되고 서술은 보존된다. + $first = $scaffolder->mergeDocument(null, $header, [$section], ['api.me.show']); + $withProse = str_replace( + '**설명** ', + '**설명** 내 프로필을 조회한다.', + $first + ); + + $regenerated = $scaffolder->mergeDocument($withProse, $header, [$section], ['api.me.show']); + + $this->assertStringContainsString('내 프로필을 조회한다.', $regenerated); + $this->assertStringNotContainsString('curl', $regenerated); + $this->assertStringContainsString('GET /api/me HTTP/1.1', $regenerated); + } + + #[Test] + public function insert_example_blocks_는_표와_서술을_건드리지_않고_예시만_삽입한다(): void + { + $scaffolder = new ApiDocScaffolder; + + // 예시 블록이 아직 없는 기존 문서 (표에는 사람이 채운 도메인 서술 셀 포함) + $existing = <<<'MD' +# Me API 레퍼런스 + +### GET /api/me + +- **라우트명**: `api.me.show` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| uuid | string | `a231` | 외부 노출용 UUID (사람이 채운 도메인 서술) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 내 프로필을 조회한다. (사람이 채운 엔드포인트 서술) + +MD; + + $blocks = [ + 'api.me.show' => [ + 'request' => "```http\nGET /api/me HTTP/1.1\nHost: g7.example.com\nAccept: application/json\n```", + 'response' => "```http\nHTTP/1.1 200\n```\n\n```json\n{\n \"success\": true\n}\n```", + ], + ]; + + [$updated, $inserted] = $scaffolder->insertExampleBlocks($existing, $blocks); + + // 예시 2블록 삽입됨 + $this->assertSame(2, $inserted); + $this->assertStringContainsString('**요청 예시**', $updated); + $this->assertStringContainsString('**응답 예시**', $updated); + $this->assertStringContainsString('GET /api/me HTTP/1.1', $updated); + // 표의 사람 서술 셀은 불변 + $this->assertStringContainsString('외부 노출용 UUID (사람이 채운 도메인 서술)', $updated); + // 엔드포인트 서술도 불변 + $this->assertStringContainsString('내 프로필을 조회한다. (사람이 채운 엔드포인트 서술)', $updated); + // 삽입 위치: 요청 예시는 응답 필드 앞, 응답 예시는 에러 응답 앞 + $this->assertLessThan(strpos($updated, '**응답 필드**'), strpos($updated, '**요청 예시**')); + $this->assertLessThan(strpos($updated, '**에러 응답**'), strpos($updated, '**응답 예시**')); + // 예시는 @generated 블록 내부 + $this->assertLessThan(strpos($updated, '@generated:end'), strpos($updated, '**요청 예시**')); + } + + #[Test] + public function insert_example_blocks_는_재삽입_시_멱등이다(): void + { + $scaffolder = new ApiDocScaffolder; + + $existing = <<<'MD' +### GET /api/me + +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건._ + +**에러 응답** + +| 401 | + + + +MD; + + $blocks = [ + 'api.me.show' => [ + 'request' => "```http\nGET /api/me HTTP/1.1\nHost: x\n```", + 'response' => "```json\n{}\n```", + ], + ]; + + [$once] = $scaffolder->insertExampleBlocks($existing, $blocks); + [$twice] = $scaffolder->insertExampleBlocks($once, $blocks); + + // 2회 삽입해도 결과 동일 (요청/응답 예시가 중복 삽입되지 않음) + $this->assertSame($once, $twice); + $this->assertSame(1, substr_count($twice, '**요청 예시**')); + $this->assertSame(1, substr_count($twice, '**응답 예시**')); + } + + #[Test] + public function 서술에_수기_응답예시가_있으면_블록에_응답예시를_넣지_않는다(): void + { + $scaffolder = new ApiDocScaffolder; + + // @generated 밖 서술에 사람이 미리 작성한 응답 예시가 있는 문서 + // (파일 업로드·미설치 정적 예시처럼 실측 불가라 수기로 채운 경우) + $existing = <<<'MD' +### GET /api/me/card + +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건._ + +**에러 응답** + +| 401 | + + + +**설명** 카드 조회. + +**응답 예시** (정적 — 실측 불가라 사람이 작성) + +```json +{ "success": true, "data": { "id": 1 } } +``` + +MD; + + $blocks = [ + 'api.me.card' => [ + 'request' => "```http\nGET /api/me/card HTTP/1.1\nHost: api.example.com\n```", + 'response' => '', + ], + ]; + + [$updated] = $scaffolder->insertExampleBlocks($existing, $blocks); + + // 요청 예시는 블록 안에 삽입된다 + $this->assertStringContainsString('**요청 예시**', $updated); + $this->assertStringContainsString('GET /api/me/card HTTP/1.1', $updated); + // 응답 예시 헤딩은 정확히 1개 (수기 예시만, 블록 안 마커 미삽입 → 중복 헤딩 없음) + $this->assertSame(1, substr_count($updated, '**응답 예시**')); + // 수기 응답 예시는 보존 + $this->assertStringContainsString('정적 — 실측 불가라 사람이 작성', $updated); + // 블록 안에 실측 제외 마커가 들어가지 않아야 한다 + $this->assertStringNotContainsString('실측 제외: http-500', $updated); + } + + #[Test] + public function 과거_삽입된_블록내_응답예시는_서술에_수기예시가_생기면_제거된다(): void + { + $scaffolder = new ApiDocScaffolder; + + // 과거 run 이 블록 안에 응답 예시 마커를 넣었고, 이후 서술에 수기 응답 예시가 추가된 상태 + $existing = <<<'MD' +### GET /api/me/card + +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건._ + +**응답 예시** + + + +**에러 응답** + +| 401 | + + + +**응답 예시** (정적) + +```json +{ "success": true } +``` + +MD; + + $blocks = [ + 'api.me.card' => [ + 'request' => "```http\nGET /api/me/card HTTP/1.1\nHost: api.example.com\n```", + 'response' => '', + ], + ]; + + [$updated] = $scaffolder->insertExampleBlocks($existing, $blocks); + + // 블록 안 응답 예시가 제거되어 응답 예시 헤딩은 1개(수기 예시)만 남는다 + $this->assertSame(1, substr_count($updated, '**응답 예시**')); + $this->assertStringNotContainsString('실측 제외: http-500', $updated); + // 수기 예시는 보존 + $this->assertStringContainsString('**응답 예시** (정적)', $updated); + } + + #[Test] + public function 파일_업로드_요청_예시는_multipart_form_data_다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'POST', 'uri' => '/api/me/avatar', 'name' => 'api.me.avatar.upload', + 'controller' => 'C', 'controller_method' => 'uploadAvatar', 'permission' => null, + 'middleware' => ['auth:sanctum'], 'path_params' => [], + ]; + $request = ['request_class' => 'X', 'params' => [ + ['name' => 'avatar', 'type' => 'image', 'required' => true, 'allowed' => 'max 2048'], + ], 'hook_filters' => []]; + $probeMeta = ['status' => null, 'skipped_reason' => 'write-method', 'base_url' => 'https://g7.example.com', 'resolved_uri' => '/api/me/avatar', 'body' => null]; + + $section = $scaffolder->endpointSection($route, $request, null, $probeMeta); + + // 파일(image) 파라미터는 application/json 이 아니라 multipart/form-data 로 표기된다. + $this->assertStringContainsString('Content-Type: multipart/form-data', $section); + $this->assertStringNotContainsString('"avatar": "string"', $section); + $this->assertStringContainsString('filename=', $section); + $this->assertStringContainsString('name="avatar"', $section); + } + + #[Test] + public function ge_t_요청_예시는_query_파라미터를_url_쿼리스트링으로_붙인다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', 'uri' => '/api/search', 'name' => 'api.search.index', + 'controller' => 'C', 'controller_method' => 'index', 'permission' => null, + 'middleware' => ['api'], 'path_params' => [], + ]; + $request = ['request_class' => 'X', 'params' => [ + ['name' => 'q', 'type' => 'string', 'required' => true, 'allowed' => ''], + ['name' => 'type', 'type' => 'string', 'required' => false, 'allowed' => '`post`, `product`'], + ], 'hook_filters' => []]; + $probeMeta = ['status' => 200, 'skipped_reason' => null, 'base_url' => 'https://g7.example.com', 'resolved_uri' => '/api/search', 'body' => ['success' => true, 'data' => []]]; + + $section = $scaffolder->endpointSection($route, $request, $schema = null, $probeMeta); + + // query 파라미터가 URL 쿼리스트링으로 반영된다 (in: 열거는 첫 값 채택). + $this->assertMatchesRegularExpression('/GET \/api\/search\?[^ ]*q=/', $section); + $this->assertStringContainsString('type=post', $section); + } + + #[Test] + public function 응답_예시는_토큰_비밀번호_등_민감값을_마스킹한다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'POST', 'uri' => '/api/auth/login', 'name' => 'api.auth.login', + 'controller' => 'C', 'controller_method' => 'login', 'permission' => null, + 'middleware' => ['api'], 'path_params' => [], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['success', 'data'], 'shape' => 'object', 'fields' => [['name' => 'token', 'type' => 'string', 'sample' => '69|abc']], 'pagination' => false]; + $body = ['success' => true, 'data' => [ + 'token' => '69|NzX4qbOT4Xns28p6Ik7d7CvGiYn8kuyi8cS0J0AL5ad91266', + 'token_type' => 'Bearer', + 'user' => ['uuid' => 'abc', 'password' => '$2y$10$abcdefghijklmnop'], + ], 'message' => null, 'error' => null]; + $probeMeta = ['status' => 200, 'skipped_reason' => null, 'base_url' => 'https://g7.example.com', 'resolved_uri' => '/api/auth/login', 'body' => $body]; + + $section = $scaffolder->endpointSection($route, $request, $schema, $probeMeta); + + // 실제 토큰/비밀번호 해시는 노출되지 않고 마스킹된다. + $this->assertStringNotContainsString('NzX4qbOT4Xns28p6', $section); + $this->assertStringNotContainsString('$2y$10$abcdefghijklmnop', $section); + $this->assertStringContainsString('{MASKED}', $section); + // token_type 은 민감값이 아니므로 보존된다. + $this->assertStringContainsString('"token_type": "Bearer"', $section); + } + + #[Test] + public function 응답_예시_body의_절대_ur_l_호스트는_placeholder로_마스킹된다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', 'uri' => '/api/admin/users', 'name' => 'api.admin.users.index', + 'controller' => 'C', 'controller_method' => 'index', 'permission' => null, + 'middleware' => ['auth:sanctum'], 'path_params' => [], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['success', 'data'], 'shape' => 'collection', 'fields' => [['name' => 'id', 'type' => 'integer', 'sample' => '1']], 'pagination' => true]; + // 실측 응답의 페이지네이터 메타·콜백 URL 은 요청 base URL 의 호스트를 그대로 직렬화한다. + $body = ['success' => true, 'data' => [ + 'data' => [['id' => 1]], + 'first_page_url' => 'https://g7_2.dev/api/admin/users?page=1', + 'path' => 'https://g7_2.dev/api/admin/users', + 'callback_url' => 'https://g7_2.dev:8443/callback', + ], 'message' => null, 'error' => null]; + $probeMeta = ['status' => 200, 'skipped_reason' => null, 'base_url' => 'https://g7_2.dev', 'resolved_uri' => '/api/admin/users', 'body' => $body]; + + $section = $scaffolder->endpointSection($route, $request, $schema, $probeMeta); + + // 실측 기준 호스트(g7_2.dev)는 응답 예시 어디에도 노출되지 않고 placeholder 로 치환된다. + $this->assertStringNotContainsString('g7_2.dev', $section); + $this->assertStringContainsString('https://api.example.com/api/admin/users?page=1', $section); + // 포트가 붙은 절대 URL 도 호스트+포트 통째로 마스킹된다. + $this->assertStringContainsString('https://api.example.com/callback', $section); + } + + #[Test] + public function 파라미터_표는_path와_form_request_의_동일_이름을_중복_출력하지_않는다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', 'uri' => '/api/templates/{identifier}/assets/{path}', 'name' => 'api.templates.assets', + 'controller' => 'C', 'controller_method' => 'assets', 'permission' => null, + 'middleware' => ['api'], 'path_params' => ['identifier', 'path'], + ]; + // FormRequest 에도 identifier/path 규칙이 있는 경우 (중복 유발 조건) + $request = ['request_class' => 'X', 'params' => [ + ['name' => 'identifier', 'type' => 'string', 'required' => true, 'allowed' => ''], + ['name' => 'path', 'type' => 'string', 'required' => true, 'allowed' => ''], + ], 'hook_filters' => []]; + $probeMeta = ['status' => 200, 'skipped_reason' => null, 'base_url' => 'https://g7.example.com', 'resolved_uri' => '/api/templates/x/assets/y', 'body' => ['success' => true, 'data' => []]]; + + $section = $scaffolder->endpointSection($route, $request, null, $probeMeta); + + // identifier/path 는 path 행으로만 1회 출력 (query 중복 행 없음). + $this->assertSame(1, substr_count($section, '| identifier | path |')); + $this->assertStringNotContainsString('| identifier | query |', $section); + $this->assertSame(1, substr_count($section, '| path | path |')); + } } diff --git a/tests/Unit/Support/ApiDoc/ResourceFieldDescriberTest.php b/tests/Unit/Support/ApiDoc/ResourceFieldDescriberTest.php index e28bd087..5569f231 100644 --- a/tests/Unit/Support/ApiDoc/ResourceFieldDescriberTest.php +++ b/tests/Unit/Support/ApiDoc/ResourceFieldDescriberTest.php @@ -85,19 +85,30 @@ class ResourceFieldDescriberTest extends TestCase { $describer = new ResourceFieldDescriber; - // 맥락 의존/도메인 특수 필드는 사전/패턴에 없어야 (TODO 로 폴백) - $this->assertNull($describer->describe('roles', 'array')); - // user/templates/modules/plugins/menus 는 맥락(객체 vs 목록)에 따라 의미가 갈려 제외 - // (user 는 사용자 객체 vs 권한 그룹 객체로 다형적, updater 는 array 로 관계 객체 아님) - $this->assertNull($describer->describe('user', 'object')); - $this->assertNull($describer->describe('templates', 'array')); - // IDV/marketing 도메인 특이 필드는 제외 + // IDV/marketing 도메인 특이 필드는 제외 (사전/패턴에 없어야 — TODO 로 폴백) $this->assertNull($describer->describe('purpose', 'string')); $this->assertNull($describer->describe('channels', 'array')); $this->assertNull($describer->describe('render_hint', 'string')); - // status/type/code/category 는 값 집합이 도메인마다 달라 제외 (역할은 있으나 도메인 종속) - $this->assertNull($describer->describe('status', 'string')); + // type/code/category 는 값 집합이 도메인마다 달라 제외 (역할은 있으나 도메인 종속) $this->assertNull($describer->describe('code', 'string')); + $this->assertNull($describer->describe('type', 'string')); + $this->assertNull($describer->describe('category', 'string')); + } + + #[Test] + public function 쓰기_응답_공통_관계_실행이력_필드를_설명한다(): void + { + // 쓰기 메서드 실측 도입으로 응답에 자주 등장하는 공통 관계/실행이력/토큰 필드는 + // 도메인 무관하게 역할이 고정되므로 설명한다(TODO 방치 금지). + $describer = new ResourceFieldDescriber; + + $this->assertStringContainsString('역할', $describer->describe('roles', 'array')); + $this->assertStringContainsString('사용자', $describer->describe('user', 'object')); + $this->assertStringContainsString('토큰', $describer->describe('token', 'string')); + $this->assertStringContainsString('실행', $describer->describe('duration', 'integer')); + $this->assertStringContainsString('종료 코드', $describer->describe('exit_code', 'integer')); + // status 는 문자열/정수일 때 일반 설명(도메인 라벨은 status_label 참조) + $this->assertStringContainsString('상태 값', $describer->describe('status', 'string')); } #[Test]