From 70452745c421890096facb5a7f451388deea361d Mon Sep 17 00:00:00 2001 From: HeuJung Date: Wed, 8 Jul 2026 10:31:57 +0900 Subject: [PATCH] =?UTF-8?q?feat(api-docs):=20API=20=EB=A0=88=ED=8D=BC?= =?UTF-8?q?=EB=9F=B0=EC=8A=A4=20=EB=AC=B8=EC=84=9C=20=EC=A0=84=EB=A9=B4?= =?UTF-8?q?=ED=99=94=20=E2=80=94=20=EC=B6=94=EC=B6=9C=20=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=C2=B7=EC=A0=84=20=EB=8C=80=EC=83=81?= =?UTF-8?q?=20=EB=AC=B8=EC=84=9C=ED=99=94=C2=B7audit=20=EA=B0=95=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코드에만 존재하던 REST API 계약(약 663 엔드포인트)을 코어/확장 책임별로 분리된 마크다운 레퍼런스로 전면 문서화한다. 674개 규모에서 수기 문서는 반드시 drift 하므로 "코드 추출 → 스캐폴딩 → 사람이 서술 채움 → 하네스가 커버리지 강제" 하이브리드로 구성했다. 추출 파이프라인 (app/Support/ApiDoc): - ApiRouteInventory / FormRequestIntrospector / ApiEndpointProbe(실측 HTTP) / ResponseSchemaInferrer / ApiDocScaffolder / ColumnCommentResolver / ResourceFieldDescriber / ParameterDescriber - api:docgen 커맨드(--scope/--seed/--check/--dry-run/--base-url/--user) + 응답/파라미터 in-place 백필 커맨드 2종(재생성 없이 TODO 셀만 치환, 멱등) - ApiDocSampleSeeder 계약 + 코어/확장 시더로 실측용 완전 샘플 멱등 생성 문서화 (실측 기반, GET read-only 실호출): - 코어 291엔드포인트 35파일(docs/backend/api) + 규정 docs/backend/api-documentation.md - 확장: ecommerce(231)·board(80)·page(17)·hello_module(2)·pay_kginicis(22)· gdpr(15)·ckeditor5(2)·marketing(2)·verification_kginicis(1) - 표준 4구성(헤더·요청 파라미터·응답 필드·에러 표) + 엔드포인트 용도 서술 - 파라미터 용도·응답 필드 설명 셀 전수 채움(도메인 지식 수기) 하네스: - audit 룰 api-doc-coverage — API 표면(라우트/컨트롤러/FormRequest/Resource) 변경 시 대응 문서 미동반이면 차단. 전 대상 문서 완비로 error 승격 - file-rules 리마인더(컨트롤러/라우트 편집 시), coverage.json, dev-dashboard 카드 - docs/backend/routing.md 확장 공개 API URL 스킴 정정(/api/modules|plugins/{id}) - /AGENTS.md/docs-index 동기 --- AGENTS.md | 5 +- .../Commands/ApiDocBackfillFieldsCommand.php | 167 ++ .../Commands/ApiDocBackfillParamsCommand.php | 162 ++ app/Console/Commands/ApiDocgenCommand.php | 544 +++++ app/Contracts/ApiDoc/ApiDocSampleSeeder.php | 33 + app/Support/ApiDoc/ApiDocSampleService.php | 374 ++++ app/Support/ApiDoc/ApiDocScaffolder.php | 357 ++++ app/Support/ApiDoc/ApiEndpointProbe.php | 143 ++ app/Support/ApiDoc/ApiRouteInventory.php | 442 ++++ app/Support/ApiDoc/ColumnCommentResolver.php | 71 + .../ApiDoc/FormRequestIntrospector.php | 200 ++ app/Support/ApiDoc/ParameterDescriber.php | 394 ++++ app/Support/ApiDoc/ResourceFieldDescriber.php | 258 +++ app/Support/ApiDoc/ResponseSchemaInferrer.php | 220 ++ database/factories/UserFactory.php | 36 +- docs/README.md | 5 +- docs/backend/README.md | 1 + docs/backend/api-documentation.md | 228 +++ docs/backend/api/activity-logs.md | 143 ++ docs/backend/api/attachment.md | 46 + docs/backend/api/attachments.md | 151 ++ docs/backend/api/auth.md | 575 ++++++ docs/backend/api/avatar.md | 74 + docs/backend/api/broadcasting.md | 43 + docs/backend/api/changelog.md | 48 + docs/backend/api/core-update.md | 91 + docs/backend/api/dashboard.md | 183 ++ docs/backend/api/extensions.md | 110 + docs/backend/api/identity.md | 1013 +++++++++ docs/backend/api/language-packs.md | 500 +++++ docs/backend/api/layouts.md | 74 + docs/backend/api/license.md | 48 + docs/backend/api/locales.md | 47 + docs/backend/api/me.md | 187 ++ docs/backend/api/menus.md | 418 ++++ docs/backend/api/modules.md | 829 ++++++++ docs/backend/api/notification-channels.md | 49 + docs/backend/api/notification-definitions.md | 218 ++ docs/backend/api/notification-logs.md | 141 ++ docs/backend/api/notification-templates.md | 146 ++ docs/backend/api/notifications.md | 454 +++++ docs/backend/api/password.md | 53 + docs/backend/api/permissions.md | 52 + docs/backend/api/plugins.md | 885 ++++++++ docs/backend/api/profile.md | 155 ++ docs/backend/api/roles.md | 307 +++ docs/backend/api/schedules.md | 485 +++++ docs/backend/api/search.md | 61 + docs/backend/api/seo.md | 174 ++ docs/backend/api/settings.md | 563 +++++ docs/backend/api/templates.md | 1810 +++++++++++++++++ docs/backend/api/users.md | 607 ++++++ docs/backend/api/verify-password.md | 51 + docs/backend/routing.md | 39 +- .../gnuboard7-hello_module/docs/api/memos.md | 117 ++ .../sirsoft-board/docs/api/activity-stats.md | 50 + .../docs/api/board-activities.md | 60 + .../sirsoft-board/docs/api/board-types.md | 139 ++ .../_bundled/sirsoft-board/docs/api/board.md | 674 ++++++ .../_bundled/sirsoft-board/docs/api/boards.md | 1543 ++++++++++++++ .../sirsoft-board/docs/api/dashboard.md | 153 ++ .../sirsoft-board/docs/api/my-comments.md | 55 + .../sirsoft-board/docs/api/reports.md | 309 +++ .../sirsoft-board/docs/api/settings.md | 180 ++ .../_bundled/sirsoft-board/docs/api/users.md | 94 + .../Support/ApiDoc/ApiDocSampleService.php | 290 +++ .../Unit/Support/ApiDocSampleServiceTest.php | 112 + .../sirsoft-ecommerce/docs/api/addresses.md | 225 ++ .../sirsoft-ecommerce/docs/api/brands.md | 231 +++ .../sirsoft-ecommerce/docs/api/cart.md | 338 +++ .../sirsoft-ecommerce/docs/api/categories.md | 513 +++++ .../docs/api/category-image.md | 46 + .../sirsoft-ecommerce/docs/api/checkout.md | 163 ++ .../docs/api/claim-reasons.md | 312 +++ .../sirsoft-ecommerce/docs/api/coupons.md | 190 ++ .../sirsoft-ecommerce/docs/api/currency.md | 76 + .../sirsoft-ecommerce/docs/api/dashboard.md | 162 ++ .../docs/api/extra-fee-templates.md | 343 ++++ .../sirsoft-ecommerce/docs/api/guest.md | 179 ++ .../sirsoft-ecommerce/docs/api/inquiries.md | 320 +++ .../docs/api/mileage-transactions.md | 226 ++ .../sirsoft-ecommerce/docs/api/mileage.md | 112 + .../sirsoft-ecommerce/docs/api/options.md | 121 ++ .../sirsoft-ecommerce/docs/api/orders.md | 935 +++++++++ .../sirsoft-ecommerce/docs/api/payments.md | 46 + .../sirsoft-ecommerce/docs/api/presets.md | 145 ++ .../docs/api/product-common-infos.md | 224 ++ .../docs/api/product-image.md | 46 + .../docs/api/product-labels.md | 217 ++ .../docs/api/product-notice-templates.md | 254 +++ .../sirsoft-ecommerce/docs/api/products.md | 1366 +++++++++++++ .../docs/api/promotion-coupons.md | 411 ++++ .../docs/api/review-image.md | 46 + .../sirsoft-ecommerce/docs/api/reviews.md | 467 +++++ .../sirsoft-ecommerce/docs/api/settings.md | 324 +++ .../docs/api/shipping-carriers.md | 256 +++ .../docs/api/shipping-country.md | 76 + .../docs/api/shipping-policies.md | 389 ++++ .../sirsoft-ecommerce/docs/api/users.md | 81 + .../sirsoft-ecommerce/docs/api/wishlist.md | 104 + .../Support/ApiDoc/ApiDocSampleService.php | 511 +++++ .../Unit/Support/ApiDocSampleServiceTest.php | 120 ++ .../sirsoft-page/docs/api/attachments.md | 109 + .../_bundled/sirsoft-page/docs/api/pages.md | 490 +++++ .../Support/ApiDoc/ApiDocSampleService.php | 113 + .../Unit/Support/ApiDocSampleServiceTest.php | 65 + .../sirsoft-ckeditor5/docs/api/images.md | 58 + .../sirsoft-ckeditor5/docs/api/upload.md | 66 + .../sirsoft-gdpr/docs/api/consent-log.md | 61 + .../_bundled/sirsoft-gdpr/docs/api/consent.md | 209 ++ .../sirsoft-gdpr/docs/api/policy-versions.md | 144 ++ .../sirsoft-gdpr/docs/api/settings.md | 120 ++ .../sirsoft-marketing/docs/api/channels.md | 77 + .../sirsoft-marketing/docs/api/settings.md | 93 + .../docs/api/user-consent-injection.md | 99 + .../sirsoft-pay_kginicis/docs/api/cbt.md | 82 + .../sirsoft-pay_kginicis/docs/api/orders.md | 505 +++++ .../sirsoft-pay_kginicis/docs/api/payment.md | 169 ++ .../docs/api/transaction.md | 47 + .../sirsoft-pay_kginicis/docs/api/vbank.md | 51 + .../Support/ApiDoc/ApiDocSampleService.php | 222 ++ .../Unit/Support/ApiDocSampleServiceTest.php | 144 ++ .../docs/api/identity.md | 96 + resources/views/dev-dashboard.blade.php | 18 + .../Console/ApiDocSampleServiceTest.php | 67 + .../ApiDocBackfillFieldsCommandTest.php | 145 ++ .../ApiDocgenExtensionDiscoveryTest.php | 170 ++ .../Support/ApiDoc/ApiDocPipelineTest.php | 312 +++ .../ApiDoc/ApiRouteInventoryFallbackTest.php | 75 + .../Support/ApiDoc/ParameterDescriberTest.php | 239 +++ .../ApiDoc/ResourceFieldDescriberTest.php | 259 +++ 131 files changed, 32105 insertions(+), 21 deletions(-) create mode 100644 app/Console/Commands/ApiDocBackfillFieldsCommand.php create mode 100644 app/Console/Commands/ApiDocBackfillParamsCommand.php create mode 100644 app/Console/Commands/ApiDocgenCommand.php create mode 100644 app/Contracts/ApiDoc/ApiDocSampleSeeder.php create mode 100644 app/Support/ApiDoc/ApiDocSampleService.php create mode 100644 app/Support/ApiDoc/ApiDocScaffolder.php create mode 100644 app/Support/ApiDoc/ApiEndpointProbe.php create mode 100644 app/Support/ApiDoc/ApiRouteInventory.php create mode 100644 app/Support/ApiDoc/ColumnCommentResolver.php create mode 100644 app/Support/ApiDoc/FormRequestIntrospector.php create mode 100644 app/Support/ApiDoc/ParameterDescriber.php create mode 100644 app/Support/ApiDoc/ResourceFieldDescriber.php create mode 100644 app/Support/ApiDoc/ResponseSchemaInferrer.php create mode 100644 docs/backend/api-documentation.md create mode 100644 docs/backend/api/activity-logs.md create mode 100644 docs/backend/api/attachment.md create mode 100644 docs/backend/api/attachments.md create mode 100644 docs/backend/api/auth.md create mode 100644 docs/backend/api/avatar.md create mode 100644 docs/backend/api/broadcasting.md create mode 100644 docs/backend/api/changelog.md create mode 100644 docs/backend/api/core-update.md create mode 100644 docs/backend/api/dashboard.md create mode 100644 docs/backend/api/extensions.md create mode 100644 docs/backend/api/identity.md create mode 100644 docs/backend/api/language-packs.md create mode 100644 docs/backend/api/layouts.md create mode 100644 docs/backend/api/license.md create mode 100644 docs/backend/api/locales.md create mode 100644 docs/backend/api/me.md create mode 100644 docs/backend/api/menus.md create mode 100644 docs/backend/api/modules.md create mode 100644 docs/backend/api/notification-channels.md create mode 100644 docs/backend/api/notification-definitions.md create mode 100644 docs/backend/api/notification-logs.md create mode 100644 docs/backend/api/notification-templates.md create mode 100644 docs/backend/api/notifications.md create mode 100644 docs/backend/api/password.md create mode 100644 docs/backend/api/permissions.md create mode 100644 docs/backend/api/plugins.md create mode 100644 docs/backend/api/profile.md create mode 100644 docs/backend/api/roles.md create mode 100644 docs/backend/api/schedules.md create mode 100644 docs/backend/api/search.md create mode 100644 docs/backend/api/seo.md create mode 100644 docs/backend/api/settings.md create mode 100644 docs/backend/api/templates.md create mode 100644 docs/backend/api/users.md create mode 100644 docs/backend/api/verify-password.md create mode 100644 modules/_bundled/gnuboard7-hello_module/docs/api/memos.md create mode 100644 modules/_bundled/sirsoft-board/docs/api/activity-stats.md create mode 100644 modules/_bundled/sirsoft-board/docs/api/board-activities.md create mode 100644 modules/_bundled/sirsoft-board/docs/api/board-types.md create mode 100644 modules/_bundled/sirsoft-board/docs/api/board.md create mode 100644 modules/_bundled/sirsoft-board/docs/api/boards.md create mode 100644 modules/_bundled/sirsoft-board/docs/api/dashboard.md create mode 100644 modules/_bundled/sirsoft-board/docs/api/my-comments.md create mode 100644 modules/_bundled/sirsoft-board/docs/api/reports.md create mode 100644 modules/_bundled/sirsoft-board/docs/api/settings.md create mode 100644 modules/_bundled/sirsoft-board/docs/api/users.md create mode 100644 modules/_bundled/sirsoft-board/src/Support/ApiDoc/ApiDocSampleService.php create mode 100644 modules/_bundled/sirsoft-board/tests/Unit/Support/ApiDocSampleServiceTest.php create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/addresses.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/brands.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/cart.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/categories.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/category-image.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/checkout.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/claim-reasons.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/coupons.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/currency.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/dashboard.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/extra-fee-templates.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/guest.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/inquiries.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/mileage-transactions.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/mileage.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/options.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/orders.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/payments.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/presets.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/product-common-infos.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/product-image.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/product-labels.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/product-notice-templates.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/products.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/promotion-coupons.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/settings.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/shipping-carriers.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/shipping-country.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/shipping-policies.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/users.md create mode 100644 modules/_bundled/sirsoft-ecommerce/docs/api/wishlist.md create mode 100644 modules/_bundled/sirsoft-ecommerce/src/Support/ApiDoc/ApiDocSampleService.php create mode 100644 modules/_bundled/sirsoft-ecommerce/tests/Unit/Support/ApiDocSampleServiceTest.php create mode 100644 modules/_bundled/sirsoft-page/docs/api/attachments.md create mode 100644 modules/_bundled/sirsoft-page/docs/api/pages.md create mode 100644 modules/_bundled/sirsoft-page/src/Support/ApiDoc/ApiDocSampleService.php create mode 100644 modules/_bundled/sirsoft-page/tests/Unit/Support/ApiDocSampleServiceTest.php create mode 100644 plugins/_bundled/sirsoft-ckeditor5/docs/api/images.md create mode 100644 plugins/_bundled/sirsoft-ckeditor5/docs/api/upload.md create mode 100644 plugins/_bundled/sirsoft-gdpr/docs/api/consent-log.md create mode 100644 plugins/_bundled/sirsoft-gdpr/docs/api/consent.md create mode 100644 plugins/_bundled/sirsoft-gdpr/docs/api/policy-versions.md create mode 100644 plugins/_bundled/sirsoft-gdpr/docs/api/settings.md create mode 100644 plugins/_bundled/sirsoft-marketing/docs/api/channels.md create mode 100644 plugins/_bundled/sirsoft-marketing/docs/api/settings.md create mode 100644 plugins/_bundled/sirsoft-marketing/docs/api/user-consent-injection.md create mode 100644 plugins/_bundled/sirsoft-pay_kginicis/docs/api/cbt.md create mode 100644 plugins/_bundled/sirsoft-pay_kginicis/docs/api/orders.md create mode 100644 plugins/_bundled/sirsoft-pay_kginicis/docs/api/payment.md create mode 100644 plugins/_bundled/sirsoft-pay_kginicis/docs/api/transaction.md create mode 100644 plugins/_bundled/sirsoft-pay_kginicis/docs/api/vbank.md create mode 100644 plugins/_bundled/sirsoft-pay_kginicis/src/Support/ApiDoc/ApiDocSampleService.php create mode 100644 plugins/_bundled/sirsoft-pay_kginicis/tests/Unit/Support/ApiDocSampleServiceTest.php create mode 100644 plugins/_bundled/sirsoft-verification_kginicis/docs/api/identity.md create mode 100644 tests/Feature/Console/ApiDocSampleServiceTest.php create mode 100644 tests/Unit/Console/ApiDocBackfillFieldsCommandTest.php create mode 100644 tests/Unit/Console/ApiDocgenExtensionDiscoveryTest.php create mode 100644 tests/Unit/Support/ApiDoc/ApiDocPipelineTest.php create mode 100644 tests/Unit/Support/ApiDoc/ApiRouteInventoryFallbackTest.php create mode 100644 tests/Unit/Support/ApiDoc/ParameterDescriberTest.php create mode 100644 tests/Unit/Support/ApiDoc/ResourceFieldDescriberTest.php diff --git a/AGENTS.md b/AGENTS.md index 4ffdec6a..b7f70b5c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,13 +6,14 @@ -### 백엔드 [backend/](docs/backend/) (31개) +### 백엔드 [backend/](docs/backend/) (32개) | 문서 | 설명 | TL;DR 핵심 | |------|------|-----------| | [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-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) | @@ -914,7 +915,7 @@ php artisan migrate:rollback | 수정 대상 파일 패턴 | 작업 전 필수 참조 | | ------------------- | ------------------ | -| `app/Http/Controllers/**` | [controllers.md](docs/backend/controllers.md) | +| `app/Http/Controllers/**` | [controllers.md](docs/backend/controllers.md), [api-documentation.md](docs/backend/api-documentation.md) | | `app/Services/**` | [service-repository.md](docs/backend/service-repository.md) | | `app/Http/Requests/**` | [validation.md](docs/backend/validation.md) | | `app/Repositories/**` | [service-repository.md](docs/backend/service-repository.md) | diff --git a/app/Console/Commands/ApiDocBackfillFieldsCommand.php b/app/Console/Commands/ApiDocBackfillFieldsCommand.php new file mode 100644 index 00000000..ba9b3a86 --- /dev/null +++ b/app/Console/Commands/ApiDocBackfillFieldsCommand.php @@ -0,0 +1,167 @@ +` 셀만 in-place 치환합니다. + * + * 채움 규칙 SSoT 는 ApiDocScaffolder::responseFieldTable 과 동일한 ResourceFieldDescriber + * 입니다 — 이후 정상 재생성(실측 서버 가동 시)도 같은 설명을 산출하므로 멱등합니다. + * + * 도메인 특이 필드(ResourceFieldDescriber 가 null 반환)는 TODO 를 그대로 둡니다. + * 응답 필드 표만 대상으로 하며, 파라미터 표(``)는 건드리지 않습니다. + */ +class ApiDocBackfillFieldsCommand extends Command +{ + /** + * @var string 커맨드 시그니처 + */ + protected $signature = 'api:docgen-backfill-fields + {--dry-run : 치환하지 않고 채울 건수만 리포트}'; + + /** + * @var string 커맨드 설명 + */ + protected $description = '기존 API 문서의 응답 필드 설명 TODO 를 리소스 계약 사전 설명으로 소급 채웁니다'; + + /** + * @var string 응답 필드 표를 식별하는 고유 헤더 + */ + private const FIELD_TABLE_HEADER = '| 필드 | 타입 | 실측 예시값 | 용도/설명 |'; + + /** + * @var string 응답 필드 설명 TODO 마커 + */ + private const TODO_MARKER = ''; + + /** + * 커맨드를 실행합니다. + * + * @return int 종료 코드 + */ + public function handle(): int + { + $describer = new ResourceFieldDescriber; + $dryRun = (bool) $this->option('dry-run'); + + $filled = 0; + $skipped = 0; + $filesChanged = 0; + + foreach ($this->apiDocFiles() as $file) { + $content = file_get_contents($file); + $result = $this->backfillFile($content, $describer, $filled, $skipped); + + if ($result !== $content) { + $filesChanged++; + if (! $dryRun) { + file_put_contents($file, $result); + } + } + } + + $prefix = $dryRun ? '[dry-run] ' : ''; + $this->info("{$prefix}응답 필드 설명 채움: {$filled}건, TODO 유지(도메인 특이): {$skipped}건, 변경 파일: {$filesChanged}개"); + + return self::SUCCESS; + } + + /** + * 대상 API 문서 파일 경로를 수집합니다. + * + * @return array 파일 경로 목록 + */ + private function apiDocFiles(): array + { + $dirs = array_filter([ + base_path('docs/backend/api'), + ...glob(base_path('modules/_bundled/*/docs/api')), + ...glob(base_path('plugins/_bundled/*/docs/api')), + ], 'is_dir'); + + if ($dirs === []) { + return []; + } + + $files = []; + foreach (Finder::create()->files()->in($dirs)->name('*.md') as $f) { + $files[] = $f->getRealPath(); + } + + return $files; + } + + /** + * 단일 문서의 응답 필드 표 TODO 를 채웁니다. + * + * 응답 필드 표 헤더 이후 표 행만 대상으로 하며, `` 셀을 + * ResourceFieldDescriber 결과로 치환합니다. 설명이 null(도메인 특이)이면 유지합니다. + * 파라미터 표는 헤더가 달라 진입하지 않으므로 건드리지 않습니다. + * + * @param string $content 문서 내용 + * @param ResourceFieldDescriber $describer 설명기 + * @param int $filled 채운 건수 (참조 누적) + * @param int $skipped 유지 건수 (참조 누적) + * @return string 치환된 문서 내용 + */ + private function backfillFile(string $content, ResourceFieldDescriber $describer, int &$filled, int &$skipped): string + { + $lines = explode("\n", $content); + $inFieldTable = false; + + foreach ($lines as $i => $line) { + // 응답 필드 표 헤더 진입 감지 + if (str_contains($line, self::FIELD_TABLE_HEADER)) { + $inFieldTable = true; + + continue; + } + + // 표 구분선(| --- | ...) 은 건너뜀 + if ($inFieldTable && preg_match('/^\|\s*-+\s*\|/', $line)) { + continue; + } + + // 표가 아닌 라인(빈 줄 또는 | 로 시작하지 않음) → 표 종료 + if ($inFieldTable && ! str_starts_with(ltrim($line), '|')) { + $inFieldTable = false; + + continue; + } + + if (! $inFieldTable || ! str_contains($line, self::TODO_MARKER)) { + continue; + } + + // 응답 필드 행 파싱: | name | type | `sample` | | + $cells = array_map('trim', explode('|', trim($line, '| '))); + if (count($cells) < 4) { + continue; + } + + [$name, $type] = [$cells[0], $cells[1]]; + $desc = $describer->describe($name, $type); + + if ($desc === null) { + $skipped++; + + continue; + } + + $safe = str_replace(['|', "\n", "\r"], ['\\|', ' ', ''], $desc); + $lines[$i] = str_replace(self::TODO_MARKER, $safe, $line); + $filled++; + } + + return implode("\n", $lines); + } +} diff --git a/app/Console/Commands/ApiDocBackfillParamsCommand.php b/app/Console/Commands/ApiDocBackfillParamsCommand.php new file mode 100644 index 00000000..0b7c8e79 --- /dev/null +++ b/app/Console/Commands/ApiDocBackfillParamsCommand.php @@ -0,0 +1,162 @@ +` 셀만 in-place 치환합니다. + * 채움 규칙 SSoT 는 ApiDocScaffolder 와 동일한 ParameterDescriber 입니다 — + * 이후 정상 재생성(실측 서버 가동 시)도 같은 설명을 산출하므로 멱등합니다. + * + * 도메인 특이 파라미터(ParameterDescriber 가 null 반환)는 TODO 를 그대로 둡니다. + */ +class ApiDocBackfillParamsCommand extends Command +{ + /** + * @var string 커맨드 시그니처 + */ + protected $signature = 'api:docgen-backfill-params + {--dry-run : 치환하지 않고 채울 건수만 리포트}'; + + /** + * @var string 커맨드 설명 + */ + protected $description = '기존 API 문서의 파라미터 용도 TODO 를 공통 파라미터 설명으로 소급 채웁니다'; + + /** + * @var string 파라미터 표를 식별하는 고유 헤더 + */ + private const PARAM_TABLE_HEADER = '| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |'; + + /** + * @var string 파라미터 용도 TODO 마커 + */ + private const TODO_MARKER = ''; + + /** + * 커맨드를 실행합니다. + * + * @return int 종료 코드 + */ + public function handle(): int + { + $describer = new ParameterDescriber; + $dryRun = (bool) $this->option('dry-run'); + + $filled = 0; + $skipped = 0; + $filesChanged = 0; + + foreach ($this->apiDocFiles() as $file) { + $content = file_get_contents($file); + $result = $this->backfillFile($content, $describer, $filled, $skipped); + + if ($result !== $content) { + $filesChanged++; + if (! $dryRun) { + file_put_contents($file, $result); + } + } + } + + $prefix = $dryRun ? '[dry-run] ' : ''; + $this->info("{$prefix}파라미터 용도 채움: {$filled}건, TODO 유지(도메인 특이): {$skipped}건, 변경 파일: {$filesChanged}개"); + + return self::SUCCESS; + } + + /** + * 대상 API 문서 파일 경로를 수집합니다. + * + * @return array 파일 경로 목록 + */ + private function apiDocFiles(): array + { + $dirs = array_filter([ + base_path('docs/backend/api'), + ...glob(base_path('modules/_bundled/*/docs/api')), + ...glob(base_path('plugins/_bundled/*/docs/api')), + ], 'is_dir'); + + if ($dirs === []) { + return []; + } + + $files = []; + foreach (Finder::create()->files()->in($dirs)->name('*.md') as $f) { + $files[] = $f->getRealPath(); + } + + return $files; + } + + /** + * 단일 문서의 파라미터 표 TODO 를 채웁니다. + * + * 파라미터 표 헤더 이후 표 행만 대상으로 하며, `` 셀을 + * ParameterDescriber 결과로 치환합니다. 설명이 null(도메인 특이)이면 유지합니다. + * + * @param string $content 문서 내용 + * @param ParameterDescriber $describer 설명기 + * @param int $filled 채운 건수 (참조 누적) + * @param int $skipped 유지 건수 (참조 누적) + * @return string 치환된 문서 내용 + */ + private function backfillFile(string $content, ParameterDescriber $describer, int &$filled, int &$skipped): string + { + $lines = explode("\n", $content); + $inParamTable = false; + + foreach ($lines as $i => $line) { + // 파라미터 표 헤더 진입 감지 + if (str_contains($line, self::PARAM_TABLE_HEADER)) { + $inParamTable = true; + + continue; + } + + // 표 구분선(| --- | ...) 은 건너뜀 + if ($inParamTable && preg_match('/^\|\s*-+\s*\|/', $line)) { + continue; + } + + // 표가 아닌 라인(빈 줄 또는 | 로 시작하지 않음) → 표 종료 + if ($inParamTable && ! str_starts_with(ltrim($line), '|')) { + $inParamTable = false; + + continue; + } + + if (! $inParamTable || ! str_contains($line, self::TODO_MARKER)) { + continue; + } + + // 파라미터 행 파싱: | name | location | type | 필수 | 허용값 | | + $cells = array_map('trim', explode('|', trim($line, '| '))); + if (count($cells) < 5) { + continue; + } + + [$name, $location, $type] = [$cells[0], $cells[1], $cells[2]]; + $desc = $describer->describe($name, $location, $type); + + if ($desc === null) { + $skipped++; + + continue; + } + + $safe = str_replace(['|', "\n", "\r"], ['\\|', ' ', ''], $desc); + $lines[$i] = str_replace(self::TODO_MARKER, $safe, $line); + $filled++; + } + + return implode("\n", $lines); + } +} diff --git a/app/Console/Commands/ApiDocgenCommand.php b/app/Console/Commands/ApiDocgenCommand.php new file mode 100644 index 00000000..aae951fc --- /dev/null +++ b/app/Console/Commands/ApiDocgenCommand.php @@ -0,0 +1,544 @@ +option('scope'); + $routes = $inventory->collect($scope); + + if ($routes === []) { + $this->warn("범위 '{$scope}' 에 해당하는 API 라우트가 없습니다."); + + return self::SUCCESS; + } + + $this->info(count($routes)."개 라우트 수집 (scope={$scope})"); + + // 도메인 파일 단위로 그룹핑 + $grouped = []; + foreach ($routes as $route) { + $file = $this->targetFile($route); + $grouped[$file][] = $route; + } + + if ($this->option('dry-run')) { + foreach ($grouped as $file => $items) { + $this->line(sprintf(' %s (%d endpoints)', $file, count($items))); + } + + return self::SUCCESS; + } + + // --seed: 도메인별 완전 샘플을 시드해 상세 GET 실측 시 null 응답을 최소화 + // (인증보다 먼저 수행해 완전 샘플 사용자로 토큰을 발급 → /me 응답도 채워짐) + $sampleMap = []; + if ($this->option('seed')) { + if (app()->environment('production')) { + $this->error('--seed 는 개발 환경 전용입니다 (production 차단).'); + + return self::FAILURE; + } + + // 코어 샘플 우선 시드 — 확장 샘플의 소유자/actor 로 쓰이는 완전 사용자를 확보한다. + $sampleMap = (new ApiDocSampleService)->seed(); + $this->info('완전 샘플 시드: '.count($sampleMap).'개 도메인 (코어)'); + + // 확장 소유 라우트가 있으면 그 확장의 규약 시더를 발견해 도메인 샘플을 병합한다. + foreach ($this->discoverExtensionSeeders($routes) as $label => $seeder) { + $extMap = $seeder->seed(); + $sampleMap = array_merge($sampleMap, $extMap); + $this->info('완전 샘플 시드: '.count($extMap)."개 도메인 ({$label})"); + } + } + + $probe = new ApiEndpointProbe($this->option('base-url') ?: null); + // 인증 사용자: --user 명시 > 완전 샘플 사용자(/me 응답 충실) > 첫 사용자 + $authUserId = $this->option('user') + ? (int) $this->option('user') + : $this->sampleUserId($sampleMap); + $probed = $probe->authenticate($authUserId); + + if (! $probed) { + $this->warn('실측 토큰 발급 실패 — 실측 없이 정적 추출만 진행합니다.'); + } else { + $this->info('실측 기준 URL: '.$probe->baseUrl()); + } + + $stats = ['files' => 0, 'endpoints' => 0, 'probed' => 0, 'skipped' => 0]; + $checkFindings = []; + + foreach ($grouped as $file => $items) { + $sections = []; + $sectionKeys = []; + + foreach ($items as $route) { + $request = $introspector->introspect($route['controller'], $route['controller_method']); + [$schema, $probeMeta] = $this->probeEndpoint($probe, $inferrer, $route, $probed, $sampleMap); + + if ($probeMeta['skipped_reason'] === null) { + $stats['probed']++; + } else { + $stats['skipped']++; + $checkFindings[] = "{$route['method']} {$route['uri']} — {$probeMeta['skipped_reason']}"; + } + + $commentMap = $this->columnComments($route, $commentResolver, $sampleMap); + $sections[] = $scaffolder->endpointSection($route, $request, $schema, $probeMeta, $commentMap); + $sectionKeys[] = $route['name'] ?: $route['uri']; + $stats['endpoints']++; + } + + if ($this->option('check')) { + if (! File::exists($file)) { + $checkFindings[] = "문서 파일 없음: {$file}"; + } + + continue; + } + + $header = $this->documentHeader($file, $items[0]); + $existing = File::exists($file) ? File::get($file) : null; + $content = $scaffolder->mergeDocument($existing, $header, $sections, $sectionKeys); + + File::ensureDirectoryExists(dirname($file)); + File::put($file, $content); + $stats['files']++; + } + + $probe->cleanup(); + + if ($this->option('check')) { + if ($checkFindings !== []) { + $this->warn('실측 제외/문서 누락 '.count($checkFindings).'건:'); + foreach (array_slice($checkFindings, 0, 50) as $f) { + $this->line(' - '.$f); + } + + return self::FAILURE; + } + + $this->info('drift 없음.'); + + return self::SUCCESS; + } + + $this->newLine(); + $this->info(sprintf( + '완료: 파일 %d개, 엔드포인트 %d개 (실측 %d, 제외 %d)', + $stats['files'], + $stats['endpoints'], + $stats['probed'], + $stats['skipped'] + )); + + return self::SUCCESS; + } + + /** + * 수집된 라우트의 소유 확장에서 규약 위치의 샘플 시더를 발견합니다. + * + * 확장 컨트롤러 FQCN(`Modules\Vendor\Ext\Http\Controllers\...`)에서 확장 베이스 + * 네임스페이스를 추출하고, `{Base}\Support\ApiDoc\ApiDocSampleService` 규약 클래스가 + * 존재하며 ApiDocSampleSeeder 를 구현하면 인스턴스를 반환합니다. 각 확장은 1회만 처리합니다. + * + * @param array> $routes 수집된 라우트 목록 + * @return array 확장 라벨(key) => 시더 인스턴스 + */ + private function discoverExtensionSeeders(array $routes): array + { + $seeders = []; + $seen = []; + + foreach ($routes as $route) { + $owner = $route['owner']; + + if ($owner['type'] === 'core' || $owner['id'] === null) { + continue; + } + + $label = $owner['key']; + + if (isset($seen[$label])) { + continue; + } + $seen[$label] = true; + + $base = $this->extensionBaseNamespace($route['controller'] ?? null); + + if ($base === null) { + continue; + } + + $class = $base.'\\Support\\ApiDoc\\ApiDocSampleService'; + + if (class_exists($class) && is_subclass_of($class, ApiDocSampleSeeder::class)) { + $seeders[$label] = app($class); + } + } + + return $seeders; + } + + /** + * 확장 컨트롤러 FQCN 에서 확장 베이스 네임스페이스를 추출합니다. + * + * 두 컨트롤러 배치 규약을 모두 지원한다: + * - `\Http\Controllers\` (대다수 확장): `Modules\Sirsoft\Page\Http\Controllers\Admin\PageController` + * → `Modules\Sirsoft\Page` + * - `\Controllers\` (일부 플러그인): `Plugins\Sirsoft\PayKginicis\Controllers\PaymentSignatureController` + * → `Plugins\Sirsoft\PayKginicis` + * + * `\Http\Controllers\` 를 우선 판정하고, 없으면 `\Controllers\` 세그먼트 앞까지를 베이스로 본다. + * + * @param string|null $controller 컨트롤러 FQCN (클로저면 null) + * @return string|null 확장 베이스 네임스페이스 (추출 불가 시 null) + */ + private function extensionBaseNamespace(?string $controller): ?string + { + if ($controller === null) { + return null; + } + + if (Str::contains($controller, '\\Http\\Controllers\\')) { + return Str::before($controller, '\\Http\\Controllers\\'); + } + + if (Str::contains($controller, '\\Controllers\\')) { + return Str::before($controller, '\\Controllers\\'); + } + + return null; + } + + /** + * 엔드포인트를 실측하고 응답 스키마를 추론합니다. + * + * @param ApiEndpointProbe $probe 실측 프로브 + * @param ResponseSchemaInferrer $inferrer 스키마 추론기 + * @param array $route 라우트 메타데이터 + * @param bool $probed 실측 가능 여부 + * @param array $sampleMap 도메인별 대표 샘플 맵 + * @return array{0: array|null, 1: array} 스키마와 실측 메타 + */ + private function probeEndpoint(ApiEndpointProbe $probe, ResponseSchemaInferrer $inferrer, array $route, bool $probed, array $sampleMap = []): array + { + if (! $probed) { + return [null, ['status' => null, 'skipped_reason' => 'no-token']]; + } + + $uri = $this->resolvePathParams($route, $sampleMap); + $result = $probe->probe($route['method'], $uri); + + if (! $result['ok'] || $result['body'] === null) { + return [null, ['status' => $result['status'], 'skipped_reason' => $result['skipped_reason'] ?? ('http-'.$result['status'])]]; + } + + return [$inferrer->infer($result['body']), ['status' => $result['status'], 'skipped_reason' => null]]; + } + + /** + * URI 의 path 파라미터를 바인딩된 모델의 실제 레코드로 치환합니다. + * + * @param array $route 라우트 메타데이터 + * @param array $sampleMap 도메인별 대표 샘플 맵 + * @return string 치환된 URI (치환 실패 시 원본 유지 → 프로브가 실측 제외) + */ + private function resolvePathParams(array $route, array $sampleMap = []): string + { + $uri = $route['uri']; + $domain = $route['domain_group'] ?? null; + + // path 파라미터가 있으면: 완전 샘플 대표 레코드 우선, 없으면 첫 레코드 키로 치환 + foreach ($route['path_params'] as $param) { + $modelClass = $route['path_bindings'][$param] ?? null; + + // 라우트-모델 바인딩이 없는 확장 패턴(예: show(int $id))은 도메인 샘플 맵의 + // 대표 모델로 폴백한다. 단, 파라미터명이 도메인의 단수 리소스명과 일치할 때만 + // (pages/{page} → Page). {slug}/{hash}/{versionId} 등 route key 가 다른 + // 문자열/보조 파라미터에는 폴백하지 않아 잘못된 치환(404)을 피한다. + if (! $modelClass && $domain !== null && isset($sampleMap[$domain]) && $this->paramMatchesDomain($param, $domain)) { + $modelClass = $sampleMap[$domain]['model']; + } + + if (! $modelClass) { + // route-model binding 도 도메인 폴백도 없는 문자열 path 파라미터 + // (예: board 의 boards/{slug}/posts/{id}). 도메인 대표 샘플이 명시한 + // path_params 맵(param 명 => 실제 값)에 해당 param 이 있으면 그 값으로 치환한다. + // slug 라우팅을 쓰는 확장이 route key(id) 와 무관한 slug/id 조합을 + // 실측할 수 있도록 하는 일반 경로다. (파라미터명 정확 일치만 허용 → 오치환 방지) + $explicit = $domain !== null ? ($sampleMap[$domain]['path_params'][$param] ?? null) : null; + + if ($explicit !== null) { + $uri = str_replace('{'.$param.'}', (string) $explicit, $uri); + } + + continue; + } + + // 시드된 완전 샘플 중 같은 모델이 있으면 그 route key 를 우선 사용 + $value = $this->sampleKeyForModel($modelClass, $sampleMap) ?? $this->firstRouteKey($modelClass); + + if ($value !== null) { + $uri = str_replace('{'.$param.'}', (string) $value, $uri); + } + } + + // 목록 GET 은 여러 행을 받아 필드별 non-null 대표 샘플을 확보한다 + // (per_page=1 이면 첫 행이 우연히 비어 "항상 null" 처럼 보임). + if ($route['method'] === 'GET' && ! Str::contains($uri, '{')) { + return $uri.(Str::contains($uri, '?') ? '&' : '?').'per_page=25'; + } + + return $uri; + } + + /** + * path 파라미터명이 도메인의 단수 리소스명과 일치하는지 판정합니다. + * + * 도메인 그룹은 복수형(pages)이고 라우트 파라미터는 단수(page)이므로, + * 파라미터명 == 도메인명 또는 파라미터명 == 도메인 단수형일 때만 매칭으로 본다. + * (page ↔ pages). {slug}/{hash}/{versionId} 는 어느 쪽과도 일치하지 않는다. + * + * @param string $param path 파라미터명 + * @param string $domain 도메인 그룹명 (복수형 가능) + * @return bool 도메인 리소스 파라미터 여부 + */ + private function paramMatchesDomain(string $param, string $domain): bool + { + return $param === $domain + || $param === Str::singular($domain) + || Str::plural($param) === $domain; + } + + /** + * 라우트의 주 모델 컬럼 주석 맵을 반환합니다 (응답 필드 설명 기본값). + * + * path 파라미터에 바인딩된 모델을 우선하고, 없으면 도메인 샘플 맵의 대표 + * 모델을 주 모델로 보고 그 테이블 주석을 사용합니다. + * + * @param array $route 라우트 메타데이터 + * @param ColumnCommentResolver $resolver 컬럼 주석 해석기 + * @param array $sampleMap 도메인별 대표 샘플 맵 + * @return array 컬럼명 => 주석 + */ + private function columnComments(array $route, ColumnCommentResolver $resolver, array $sampleMap = []): array + { + $bindings = $route['path_bindings'] ?? []; + + // 마지막 path 바인딩 모델(가장 구체적인 리소스)을 주 모델로 사용 + $modelClass = null; + foreach ($bindings as $class) { + $modelClass = $class; + } + + // path 바인딩이 없으면(목록/단건 me 등) 도메인 샘플 맵 → 정적 힌트 순으로 유추 + if (! $modelClass) { + $domain = $route['domain_group'] ?? null; + $modelClass = ($domain && isset($sampleMap[$domain]) ? $sampleMap[$domain]['model'] : null) + ?? $this->domainModelHint($domain); + } + + return $modelClass ? $resolver->forModel($modelClass) : []; + } + + /** + * 샘플 맵으로 유추되지 않는 도메인의 주 모델을 정적 힌트로 매핑합니다. + * + * me/auth/profile/password 등은 path 바인딩도 없고 sampleMap 키(users)와도 + * 도메인명이 달라 자동 유추가 안 되지만, 모두 User 필드를 반환합니다. + * + * @param string|null $domain 도메인 그룹명 + * @return class-string|null 주 모델 FQCN (없으면 null) + */ + private function domainModelHint(?string $domain): ?string + { + if ($domain === null) { + return null; + } + + $hints = [ + 'me' => User::class, + 'auth' => User::class, + 'profile' => User::class, + 'password' => User::class, + 'modules' => Module::class, + 'plugins' => Plugin::class, + 'templates' => Template::class, + ]; + + return $hints[$domain] ?? null; + } + + /** + * 완전 샘플 맵에서 샘플 사용자의 DB id 를 조회합니다 (토큰 발급 대상). + * + * @param array $sampleMap 대표 샘플 맵 + * @return int|null 샘플 사용자 id (없으면 null → 첫 사용자로 fallback) + */ + private function sampleUserId(array $sampleMap): ?int + { + $users = $sampleMap['users'] ?? null; + + if (! $users) { + return null; + } + + return User::query() + ->where($users['key'], $users['value']) + ->value('id'); + } + + /** + * 시드된 완전 샘플 맵에서 해당 모델의 대표 route key 를 찾습니다. + * + * @param class-string $modelClass 모델 FQCN + * @param array $sampleMap 대표 샘플 맵 + * @return string|null 대표 route key 값 (없으면 null) + */ + private function sampleKeyForModel(string $modelClass, array $sampleMap): ?string + { + foreach ($sampleMap as $sample) { + if ($sample['model'] === $modelClass) { + return $sample['value']; + } + } + + return null; + } + + /** + * 모델의 첫 레코드 route key 값을 반환합니다. + * + * @param class-string $modelClass 모델 FQCN + * @return mixed route key 값 (레코드 없으면 null) + */ + private function firstRouteKey(string $modelClass): mixed + { + try { + /** @var Model $model */ + $model = new $modelClass; + $keyName = $model->getRouteKeyName(); + + $record = $modelClass::query()->orderBy($model->getKeyName())->first(); + + return $record?->getAttribute($keyName); + } catch (\Throwable) { + return null; + } + } + + /** + * 라우트가 저장될 문서 파일 경로를 산출합니다. + * + * @param array $route 라우트 메타데이터 + * @return string 절대 파일 경로 + */ + private function targetFile(array $route): string + { + $owner = $route['owner']; + $domain = $route['domain_group']; + + if ($owner['type'] === 'core') { + return base_path("docs/backend/api/{$domain}.md"); + } + + $base = $owner['type'] === 'module' ? 'modules' : 'plugins'; + + return base_path("{$base}/_bundled/{$owner['id']}/docs/api/{$domain}.md"); + } + + /** + * 문서 헤더(제목 + TL;DR)를 생성합니다. + * + * @param string $file 문서 파일 경로 + * @param array $firstRoute 대표 라우트 + * @return string 문서 헤더 + */ + private function documentHeader(string $file, array $firstRoute): string + { + $domain = Str::headline(pathinfo($file, PATHINFO_FILENAME)); + $owner = $firstRoute['owner']; + $ownerLabel = $owner['type'] === 'core' ? '코어' : "{$owner['type']} `{$owner['id']}`"; + + return << **소유**: {$ownerLabel} · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + + --- + + ## TL;DR (5초 요약) + + ```text + 1. 이 문서는 실제 API 호출로 실측한 {$domain} 엔드포인트 레퍼런스입니다 + 2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 + 3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 + 4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 + 5. 설명(TODO) 칸은 사람이 채웁니다 + ``` + + --- + + + MD; + } +} diff --git a/app/Contracts/ApiDoc/ApiDocSampleSeeder.php b/app/Contracts/ApiDoc/ApiDocSampleSeeder.php new file mode 100644 index 00000000..ffe77aff --- /dev/null +++ b/app/Contracts/ApiDoc/ApiDocSampleSeeder.php @@ -0,0 +1,33 @@ + 실제 값 맵)를 두면, route-model binding + * 도 도메인 폴백도 없는 문자열 path 파라미터(예: 게시판 slug 라우팅 + * `boards/{slug}/posts/{id}`)를 param 명 정확 일치로 치환해 상세 GET 을 + * 실측할 수 있습니다. 미제공 시 route key 기반 자동 치환만 적용됩니다. + * + * @return array}> 도메인 => 대표 레코드 정보 + */ + public function seed(): array; +} diff --git a/app/Support/ApiDoc/ApiDocSampleService.php b/app/Support/ApiDoc/ApiDocSampleService.php new file mode 100644 index 00000000..4123206a --- /dev/null +++ b/app/Support/ApiDoc/ApiDocSampleService.php @@ -0,0 +1,374 @@ + 도메인 => 대표 레코드 정보 + */ + public function seed(): array + { + $map = []; + + // permissions → roles → users 순: user 가 role 을, role 이 permission 을 참조 + $map['permissions'] = $this->seedPermission(); + $map['roles'] = $this->seedRole(); + $map['users'] = $this->seedUser(); + $map['menus'] = $this->seedMenu(); + $map['notification-definitions'] = $this->seedNotificationDefinition(); + $map['schedules'] = $this->seedSchedule(); + $map['activity-logs'] = $this->seedActivityLog(); + $map['language-packs'] = $this->seedLanguagePack(); + $map['notification-logs'] = $this->seedNotificationLog(); + $map['notification-templates'] = $this->seedNotificationTemplate(); + + return array_filter($map); + } + + /** + * 완전한 사용자 샘플을 생성하고 roles 관계를 연결합니다. + * + * @return array{model: class-string, key: string, value: string} 대표 레코드 정보 + */ + private function seedUser(): array + { + $user = User::query()->where('email', self::MARKER.'-user@example.com')->first(); + + if (! $user) { + $user = User::factory()->complete()->create([ + 'name' => 'API 문서 샘플 사용자', + 'email' => self::MARKER.'-user@example.com', + ]); + + $role = Role::query()->where('identifier', self::MARKER.'-role')->first() + ?? Role::query()->where('identifier', 'admin')->first() + ?? Role::query()->first() + ?? Role::factory()->create(['identifier' => self::MARKER.'-role-fallback']); + $user->roles()->syncWithoutDetaching([$role->id]); + } + + return ['model' => User::class, 'key' => $user->getRouteKeyName(), 'value' => (string) $user->getRouteKey()]; + } + + /** + * 완전한 역할 샘플을 생성하고 permissions 관계를 연결합니다. + * + * @return array{model: class-string, key: string, value: string} 대표 레코드 정보 + */ + private function seedRole(): array + { + $role = Role::query()->where('identifier', self::MARKER.'-role')->first(); + + if (! $role) { + $role = Role::factory()->create([ + 'identifier' => self::MARKER.'-role', + 'name' => ['ko' => 'API 문서 샘플 역할', 'en' => 'API Doc Sample Role'], + 'description' => ['ko' => '문서 실측용 역할', 'en' => 'Sample role for API docs'], + 'is_active' => true, + ]); + + $permissionIds = Permission::query()->limit(3)->pluck('id')->all(); + if ($permissionIds !== []) { + $role->permissions()->syncWithoutDetaching($permissionIds); + } + } + + return ['model' => Role::class, 'key' => $role->getRouteKeyName(), 'value' => (string) $role->getRouteKey()]; + } + + /** + * 완전한 권한 샘플(부모-자식 계층)을 생성합니다. + * + * @return array{model: class-string, key: string, value: string} 대표 레코드 정보 + */ + private function seedPermission(): array + { + $permission = Permission::query()->where('identifier', self::MARKER.'.parent')->first(); + + if (! $permission) { + $permission = Permission::factory()->create([ + 'identifier' => self::MARKER.'.parent', + 'name' => ['ko' => 'API 문서 샘플 권한', 'en' => 'API Doc Sample Permission'], + 'description' => ['ko' => '문서 실측용 권한', 'en' => 'Sample permission'], + 'type' => 'admin', + ]); + + Permission::factory()->create([ + 'parent_id' => $permission->id, + 'identifier' => self::MARKER.'.child', + 'name' => ['ko' => '하위 권한', 'en' => 'Child Permission'], + 'type' => 'admin', + ]); + } + + return ['model' => Permission::class, 'key' => $permission->getRouteKeyName(), 'value' => (string) $permission->getRouteKey()]; + } + + /** + * 완전한 메뉴 샘플(부모-자식)을 생성합니다. + * + * @return array{model: class-string, key: string, value: string} 대표 레코드 정보 + */ + private function seedMenu(): array + { + $menu = Menu::query()->where('slug', self::MARKER.'-menu')->first(); + + if (! $menu) { + $creator = User::query()->where('email', self::MARKER.'-user@example.com')->first(); + + $menu = Menu::factory()->create([ + 'name' => ['ko' => 'API 문서 샘플 메뉴', 'en' => 'API Doc Sample Menu'], + 'slug' => self::MARKER.'-menu', + 'url' => '/admin/apidoc-sample', + 'icon' => 'fas fa-book', + 'is_active' => true, + 'created_by' => $creator?->id, + ]); + + Menu::factory()->create([ + 'name' => ['ko' => '하위 메뉴', 'en' => 'Child Menu'], + 'slug' => self::MARKER.'-menu-child', + 'parent_id' => $menu->id, + 'created_by' => $creator?->id, + ]); + } + + return ['model' => Menu::class, 'key' => $menu->getRouteKeyName(), 'value' => (string) $menu->getRouteKey()]; + } + + /** + * 완전한 알림 정의 샘플(템플릿 포함)을 생성합니다. + * + * @return array{model: class-string, key: string, value: string} 대표 레코드 정보 + */ + private function seedNotificationDefinition(): array + { + $definition = NotificationDefinition::query()->where('type', self::MARKER.'.event')->first(); + + if (! $definition) { + $definition = NotificationDefinition::factory()->create([ + 'type' => self::MARKER.'.event', + 'name' => ['ko' => 'API 문서 샘플 알림', 'en' => 'API Doc Sample Notification'], + 'description' => ['ko' => '문서 실측용 알림 정의', 'en' => 'Sample notification'], + 'channels' => ['database', 'mail'], + 'is_active' => true, + ]); + } + + return ['model' => NotificationDefinition::class, 'key' => $definition->getRouteKeyName(), 'value' => (string) $definition->getRouteKey()]; + } + + /** + * 완전한 스케줄 샘플(실행 이력 포함)을 생성합니다. + * + * @return array{model: class-string, key: string, value: string} 대표 레코드 정보 + */ + private function seedSchedule(): array + { + $schedule = Schedule::query()->where('name', 'API 문서 샘플 스케줄')->first(); + + if (! $schedule) { + $creator = $this->sampleUser(); + + $schedule = Schedule::create([ + 'name' => 'API 문서 샘플 스케줄', + 'description' => '문서 실측용 스케줄', + 'type' => 'artisan', + 'command' => 'cache:clear', + 'expression' => '0 3 * * *', + 'frequency' => 'daily', + 'without_overlapping' => true, + 'run_in_maintenance' => false, + 'timeout' => 300, + 'is_active' => true, + 'last_result' => 'success', + 'last_run_at' => now()->subDay(), + 'next_run_at' => now()->addDay(), + 'created_by' => $creator?->id, + ]); + } + + return ['model' => Schedule::class, 'key' => $schedule->getRouteKeyName(), 'value' => (string) $schedule->getRouteKey()]; + } + + /** + * 완전한 활동 로그 샘플(actor + changes 포함)을 생성합니다. + * + * @return array{model: class-string, key: string, value: string} 대표 레코드 정보 + */ + private function seedActivityLog(): array + { + $log = ActivityLog::query()->where('description_key', 'apidoc.sample.action')->first(); + + if (! $log) { + $user = $this->sampleUser(); + + $log = ActivityLog::create([ + 'log_type' => 'admin', + 'loggable_type' => User::class, + 'loggable_id' => $user?->id, + 'user_id' => $user?->id, + 'action' => 'user.update', + 'description_key' => 'apidoc.sample.action', + 'description_params' => ['name' => 'API 문서 샘플'], + 'properties' => ['source' => 'apidoc'], + 'changes' => ['status' => ['old' => 'inactive', 'new' => 'active']], + 'ip_address' => '127.0.0.1', + 'user_agent' => 'ApiDocgen/1.0', + 'created_at' => now(), + ]); + } + + return ['model' => ActivityLog::class, 'key' => $log->getRouteKeyName(), 'value' => (string) $log->getRouteKey()]; + } + + /** + * 완전한 언어팩 샘플(manifest 채움)을 생성합니다. + * + * @return array{model: class-string, key: string, value: string} 대표 레코드 정보 + */ + private function seedLanguagePack(): array + { + $pack = LanguagePack::query()->where('identifier', 'apidoc-sample-lang')->first(); + + if (! $pack) { + $installer = $this->sampleUser(); + + $pack = LanguagePack::create([ + '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', + 'license' => 'MIT', + 'description' => ['ko' => '문서 실측용 언어팩', 'en' => 'Sample language pack'], + 'status' => 'active', + 'is_protected' => false, + 'manifest' => [ + 'name' => ['ko' => 'API 문서 샘플 언어팩', 'en' => 'API Doc Sample Pack'], + 'version' => '1.0.0', + 'locale' => 'fr', + ], + 'source_type' => 'bundled', + 'installed_by' => $installer?->id, + 'installed_at' => now()->subDays(3), + 'activated_at' => now()->subDays(3), + ]); + } + + return ['model' => LanguagePack::class, 'key' => $pack->getRouteKeyName(), 'value' => (string) $pack->getRouteKey()]; + } + + /** + * 완전한 알림 로그 샘플(수신자/발신자 포함)을 생성합니다. + * + * @return array{model: class-string, key: string, value: string} 대표 레코드 정보 + */ + private function seedNotificationLog(): array + { + $log = NotificationLog::query()->where('notification_type', 'apidoc.sample.event')->first(); + + if (! $log) { + $user = $this->sampleUser(); + + $log = NotificationLog::create([ + 'channel' => 'mail', + 'notification_type' => 'apidoc.sample.event', + 'extension_type' => 'core', + 'extension_identifier' => '', + 'recipient_user_id' => $user?->id, + 'recipient_identifier' => $user?->email, + 'recipient_name' => $user?->name, + 'sender_user_id' => $user?->id, + 'subject' => 'API 문서 샘플 알림', + 'body' => '문서 실측용 알림 본문입니다.', + 'status' => 'sent', + 'error_message' => null, + 'source' => 'apidoc', + 'sent_at' => now()->subHour(), + ]); + } + + return ['model' => NotificationLog::class, 'key' => $log->getRouteKeyName(), 'value' => (string) $log->getRouteKey()]; + } + + /** + * 완전한 알림 템플릿 샘플(정의 연결)을 생성합니다. + * + * @return array{model: class-string, key: string, value: string} 대표 레코드 정보 + */ + private function seedNotificationTemplate(): array + { + $this->seedNotificationDefinition(); + $definitionId = NotificationDefinition::query()->where('type', self::MARKER.'.event')->value('id'); + + $template = NotificationTemplate::query() + ->where('definition_id', $definitionId) + ->where('channel', 'mail') + ->first(); + + if (! $template && $definitionId) { + $updater = $this->sampleUser(); + + $template = NotificationTemplate::create([ + 'definition_id' => $definitionId, + 'channel' => 'mail', + 'subject' => 'API 문서 샘플 템플릿 제목', + 'body' => '안녕하세요 {{name}} 님, 문서 실측용 본문입니다.', + 'click_url' => '/admin/apidoc-sample', + 'recipients' => [['type' => 'role', 'value' => 'admin']], + 'is_active' => true, + 'is_default' => false, + 'updated_by' => $updater?->id, + ]); + } + + if (! $template) { + return []; + } + + return ['model' => NotificationTemplate::class, 'key' => $template->getRouteKeyName(), 'value' => (string) $template->getRouteKey()]; + } + + /** + * 시드된 완전 샘플 사용자를 반환합니다 (연관 엔티티의 소유자/actor 용). + * + * @return User|null 샘플 사용자 (없으면 null) + */ + private function sampleUser(): ?User + { + return User::query()->where('email', self::MARKER.'-user@example.com')->first(); + } +} diff --git a/app/Support/ApiDoc/ApiDocScaffolder.php b/app/Support/ApiDoc/ApiDocScaffolder.php new file mode 100644 index 00000000..f39de45c --- /dev/null +++ b/app/Support/ApiDoc/ApiDocScaffolder.php @@ -0,0 +1,357 @@ +'; + + /** + * @param ResourceFieldDescriber $fieldDescriber accessor/computed 필드 설명기 + * @param ParameterDescriber $paramDescriber 공통 요청 파라미터 설명기 + */ + public function __construct( + private readonly ResourceFieldDescriber $fieldDescriber = new ResourceFieldDescriber, + private readonly ParameterDescriber $paramDescriber = new ParameterDescriber + ) {} + + /** + * 단일 엔드포인트의 마크다운 섹션을 생성합니다. + * + * @param array $route 라우트 메타데이터 + * @param array $request FormRequest 분석 결과 + * @param array|null $schema 실측 응답 스키마 (null=실측 안 됨) + * @param array $probeMeta 실측 메타 (status, skipped_reason) + * @param array $commentMap 컬럼명 => 주석 (필드 설명 기본값) + * @return string 마크다운 섹션 + */ + public function endpointSection(array $route, array $request, ?array $schema, array $probeMeta, array $commentMap = []): string + { + $name = $route['name'] ?: '(unnamed)'; + $heading = "### {$route['method']} {$route['uri']}"; + $genKey = $name; + + $lines = []; + $lines[] = $heading; + $lines[] = self::GEN_START.$genKey.' -->'; + $lines[] = "- **라우트명**: `{$name}`"; + if ($route['controller']) { + $lines[] = "- **컨트롤러**: `{$route['controller']}@{$route['controller_method']}`"; + } + $lines[] = '- **인증/권한**: '.$this->authLine($route); + $lines[] = ''; + + $lines[] = '**요청 파라미터**'; + $lines[] = ''; + $lines[] = $this->requestParamTable($route, $request); + if (! empty($request['hook_filters'])) { + $hooks = implode('`, `', $request['hook_filters']); + $lines[] = ''; + $lines[] = "> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`{$hooks}`)."; + } + $lines[] = ''; + + $lines[] = '**응답 필드** (`data` 내부)'; + $lines[] = ''; + $lines[] = $this->responseFieldTable($schema, $probeMeta, $commentMap); + $lines[] = ''; + + $lines[] = '**에러 응답**'; + $lines[] = ''; + $lines[] = $this->errorTable($route, $request); + $lines[] = ''; + $lines[] = self::GEN_END; + $lines[] = ''; + $lines[] = '**설명** '; + $lines[] = ''; + + return implode("\n", $lines)."\n"; + } + + /** + * 인증/권한 라인을 구성합니다. + * + * @param array $route 라우트 메타데이터 + * @return string 인증/권한 설명 + */ + private function authLine(array $route): string + { + $mw = $route['middleware'] ?? []; + $parts = []; + + // optional.sanctum(회원/비회원 모두 접근 — Bearer 토큰 있으면 인증, 없으면 guest)은 + // auth:sanctum(인증 필수)과 계약이 다르므로 별도 표기한다. 'sanctum' 부분일치가 + // optional.sanctum 까지 auth:sanctum 으로 오표기하던 회귀를 막는다. + if ($this->hasMiddleware($mw, 'optional.sanctum')) { + $parts[] = '`optional.sanctum` (선택적 인증: 회원/비회원 모두 접근)'; + } elseif ($this->hasMiddleware($mw, 'sanctum')) { + $parts[] = '`auth:sanctum`'; + } + if ($this->hasMiddleware($mw, 'AdminMiddleware')) { + $parts[] = '`admin`'; + } + if ($route['permission']) { + $parts[] = "`permission:{$route['permission']}`"; + } + + return $parts === [] ? '공개 (인증 불필요)' : implode(' + ', $parts); + } + + /** + * 미들웨어 목록에 특정 토큰이 포함되는지 확인합니다. + * + * @param array $middleware 미들웨어 목록 + * @param string $needle 검색 토큰 + * @return bool 포함 여부 + */ + private function hasMiddleware(array $middleware, string $needle): bool + { + foreach ($middleware as $mw) { + if (Str::contains($mw, $needle)) { + return true; + } + } + + return false; + } + + /** + * 요청 파라미터 표를 생성합니다. + * + * @param array $route 라우트 메타데이터 + * @param array $request FormRequest 분석 결과 + * @return string 마크다운 표 + */ + private function requestParamTable(array $route, array $request): string + { + $rows = []; + + foreach ($route['path_params'] as $pathParam) { + $desc = $this->paramDescriber->describe($pathParam, 'path', 'string'); + $descCell = $desc !== null ? $this->escapeCell($desc) : ''; + $rows[] = "| {$pathParam} | path | string | 예 | — | {$descCell} |"; + } + + $location = in_array($route['method'], ['GET', 'DELETE'], true) ? 'query' : 'body'; + + foreach ($request['params'] as $p) { + $required = $p['required'] ? '예' : '아니오'; + $allowed = $p['allowed'] !== '' ? $p['allowed'] : '—'; + $desc = $this->paramDescriber->describe($p['name'], $location, $p['type']); + $descCell = $desc !== null ? $this->escapeCell($desc) : ''; + $rows[] = "| {$p['name']} | {$location} | {$p['type']} | {$required} | {$allowed} | {$descCell} |"; + } + + if ($rows === []) { + return '_요청 파라미터 없음._'; + } + + return "| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |\n| --- | --- | --- | --- | --- | --- |\n".implode("\n", $rows); + } + + /** + * 응답 필드 표를 생성합니다. + * + * @param array|null $schema 실측 응답 스키마 + * @param array $probeMeta 실측 메타 + * @param array $commentMap 컬럼명 => 주석 (필드 설명 기본값) + * @return string 마크다운 표 또는 실측 제외 사유 + */ + private function responseFieldTable(?array $schema, array $probeMeta, array $commentMap = []): string + { + if ($schema === null) { + $reason = $probeMeta['skipped_reason'] ?? 'not-probed'; + + return ""; + } + + $note = ''; + if ($schema['shape'] === 'collection') { + $note = '_목록 응답: `data.data[]` 배열 항목의 필드'.($schema['pagination'] ? ' + `data.pagination`' : '').'._'; + } elseif ($schema['shape'] === 'object') { + $note = '_단건 응답: `data` 객체의 필드._'; + } + + if ($schema['fields'] === []) { + return $note."\n\n"; + } + + $rows = []; + foreach ($schema['fields'] as $f) { + // 필드 설명 우선순위: + // 1) 리소스 계약 사전 (accessor/computed — status_label, is_owner, *_at 등) + // 2) 컬럼 주석 (한국어 comment — 테이블 실제 컬럼) + // 3) TODO (사람 보강) + // 계약 사전이 앞서는 이유: created_at 은 어느 테이블이든 "생성 일시" 이고, + // status_label 은 컬럼이 아니라 Enum label() 산물이라 주석이 없기 때문. + $desc = $this->fieldDescriber->describe($f['name'], $f['type'] ?? '') + ?? ($commentMap[$f['name']] ?? null); + $descCell = $desc !== null ? $this->escapeCell($desc) : ''; + $rows[] = "| {$f['name']} | {$f['type']} | `{$f['sample']}` | {$descCell} |"; + } + + $table = "| 필드 | 타입 | 실측 예시값 | 용도/설명 |\n| --- | --- | --- | --- |\n".implode("\n", $rows); + + return $note !== '' ? $note."\n\n".$table : $table; + } + + /** + * 에러 응답 표를 생성합니다. + * + * 라우트 메타에서 대표 에러 상태코드와 발생 조건을 자동 추론합니다. + * - 401: 인증 필수(`auth:sanctum`) 미들웨어. `optional.sanctum`(선택 인증)은 제외. + * - 403: `admin` 미들웨어 또는 `permission:` 요구 → 권한 부족 시. + * - 422: FormRequest 검증 규칙 존재 → 검증 실패 시. + * - 404: path 파라미터 존재 → 대상 리소스 미발견 시. + * + * 자동 추론은 대표 상태코드의 초안이며, 도메인 특이 에러(409 충돌·429 제한 등)는 + * `@generated` 블록 밖 사람 서술에서 보강한다. + * + * @param array $route 라우트 메타데이터 + * @param array $request FormRequest 분석 결과 + * @return string 마크다운 표 + */ + private function errorTable(array $route, array $request): string + { + $mw = $route['middleware'] ?? []; + $rows = []; + + // 401: 인증 필수. optional.sanctum(선택 인증)은 미인증도 허용하므로 제외. + if (! $this->hasMiddleware($mw, 'optional.sanctum') && $this->hasMiddleware($mw, 'sanctum')) { + $rows[] = '| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |'; + } + + // 403: admin 게이트 또는 permission 요구 → 권한 부족. + if ($this->hasMiddleware($mw, 'AdminMiddleware') || ! empty($route['permission'])) { + $cond = ! empty($route['permission']) + ? "요구 권한(`{$route['permission']}`)이 없는 경우" + : '관리자 권한이 없는 경우'; + $rows[] = "| 403 | Forbidden | {$cond} |"; + } + + // 422: FormRequest 검증 규칙 존재 → 검증 실패. (훅 주입 규칙 포함 가능) + $hasValidation = ! empty($request['params']) || ! empty($request['hook_filters']); + if ($hasValidation) { + $rows[] = '| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |'; + } + + // 404: path 파라미터 존재 → 대상 리소스 미발견. + if (! empty($route['path_params'])) { + $rows[] = '| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |'; + } + + if ($rows === []) { + return '_대표 에러 없음 (공개 조회). _'; + } + + return "| 상태코드 | 의미 | 발생 조건 |\n| --- | --- | --- |\n".implode("\n", $rows); + } + + /** + * 마크다운 표 셀 안에서 안전하도록 파이프/개행을 이스케이프합니다. + * + * @param string $text 원본 텍스트 + * @return string 이스케이프된 텍스트 + */ + private function escapeCell(string $text): string + { + return str_replace(['|', "\n", "\r"], ['\\|', ' ', ''], $text); + } + + /** + * 기존 문서에 새 생성 블록을 병합합니다. 사람 서술은 보존합니다. + * + * @param string|null $existing 기존 문서 내용 (null=신규) + * @param string $header 문서 헤더 (제목 + TL;DR 등) + * @param array $sections 엔드포인트 섹션 목록 (라우트명 순) + * @param array $sectionKeys 각 섹션의 라우트명 키 + * @return string 병합된 문서 내용 + */ + public function mergeDocument(?string $existing, string $header, array $sections, array $sectionKeys): string + { + if ($existing === null) { + return $header."\n".implode("\n", $sections); + } + + $merged = $header."\n"; + + foreach ($sections as $i => $section) { + $key = $sectionKeys[$i]; + $preserved = $this->extractHumanProse($existing, $key); + $merged .= $this->applyPreservedProse($section, $preserved)."\n"; + } + + return $merged; + } + + /** + * 기존 문서에서 특정 엔드포인트의 사람 서술(생성 블록 밖)을 추출합니다. + * + * @param string $existing 기존 문서 + * @param string $key 라우트명 키 + * @return string|null 보존할 사람 서술 (없으면 null) + */ + private function extractHumanProse(string $existing, string $key): ?string + { + $startMarker = self::GEN_START.$key.' -->'; + $startPos = strpos($existing, $startMarker); + + if ($startPos === false) { + return null; + } + + $endPos = strpos($existing, self::GEN_END, $startPos); + if ($endPos === false) { + return null; + } + + $afterGen = substr($existing, $endPos + strlen(self::GEN_END)); + // 다음 ### 헤딩 전까지가 이 엔드포인트의 사람 서술 + $nextHeading = preg_match('/\n### /', $afterGen, $m, PREG_OFFSET_CAPTURE) + ? $m[0][1] + : strlen($afterGen); + + $prose = trim(substr($afterGen, 0, $nextHeading)); + + // 기본 TODO 스텁만 있으면 보존할 것 없음 + if ($prose === '' || Str::contains($prose, 'TODO: 이 엔드포인트의 용도')) { + return null; + } + + return $prose; + } + + /** + * 새 섹션의 기본 서술 스텁을 보존된 사람 서술로 치환합니다. + * + * @param string $section 새로 생성된 섹션 + * @param string|null $preserved 보존할 사람 서술 + * @return string 서술이 반영된 섹션 + */ + private function applyPreservedProse(string $section, ?string $preserved): string + { + if ($preserved === null) { + return $section; + } + + $stub = '**설명** '; + + return str_replace($stub, $preserved, $section); + } +} diff --git a/app/Support/ApiDoc/ApiEndpointProbe.php b/app/Support/ApiDoc/ApiEndpointProbe.php new file mode 100644 index 00000000..286fa4d8 --- /dev/null +++ b/app/Support/ApiDoc/ApiEndpointProbe.php @@ -0,0 +1,143 @@ +baseUrl = rtrim($resolved, '/'); + } + + /** + * 실측 기준 URL 을 반환합니다. + * + * @return string 기준 URL + */ + public function baseUrl(): string + { + return $this->baseUrl; + } + + /** + * 실측용 관리자 토큰을 발급합니다. + * + * @param int|null $userId 토큰 발급 대상 사용자 ID (null 이면 첫 관리자) + * @return bool 발급 성공 여부 + */ + public function authenticate(?int $userId = null): bool + { + $user = $userId + ? User::find($userId) + : User::query()->orderBy('id')->first(); + + if (! $user) { + return false; + } + + $this->cleanupTokens(); + $this->token = $user->createToken($this->tokenName)->plainTextToken; + + return true; + } + + /** + * GET 엔드포인트를 실호출하여 응답을 관측합니다. + * + * @param string $method HTTP 메서드 + * @param string $uri 라우트 URI (path 파라미터 치환 완료된 실제 경로) + * @return array{ok: bool, status: int|null, body: array|null, skipped_reason: string|null} + */ + public function probe(string $method, string $uri): 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']; + } + + if (! $this->token) { + return ['ok' => false, 'status' => null, 'body' => null, 'skipped_reason' => 'no-token']; + } + + try { + $response = Http::withoutVerifying() + ->withToken($this->token) + ->acceptJson() + ->timeout(15) + ->get($this->baseUrl.$uri); + + $json = $response->json(); + + return [ + 'ok' => $response->successful() && is_array($json), + 'status' => $response->status(), + 'body' => is_array($json) ? $json : null, + 'skipped_reason' => null, + ]; + } catch (\Throwable $e) { + return ['ok' => false, 'status' => null, 'body' => null, 'skipped_reason' => 'request-failed: '.$e->getMessage()]; + } + } + + /** + * 발급한 임시 토큰을 정리합니다. + */ + public function cleanup(): void + { + $this->cleanupTokens(); + $this->token = null; + } + + /** + * 실측용 토큰 레코드를 모두 삭제합니다. + */ + private function cleanupTokens(): void + { + PersonalAccessToken::query() + ->where('name', $this->tokenName) + ->delete(); + } +} diff --git a/app/Support/ApiDoc/ApiRouteInventory.php b/app/Support/ApiDoc/ApiRouteInventory.php new file mode 100644 index 00000000..aaa27a0e --- /dev/null +++ b/app/Support/ApiDoc/ApiRouteInventory.php @@ -0,0 +1,442 @@ +> 정규화된 라우트 메타데이터 목록 + */ + public function collect(?string $scope = null): array + { + $routes = []; + + foreach (RouteFacade::getRoutes() as $route) { + /** @var Route $route */ + $uri = $route->uri(); + + if (! Str::startsWith($uri, 'api/')) { + continue; + } + + $name = $route->getName() ?? ''; + $owner = $this->resolveOwner($name, $uri); + + if ($scope !== null && ! $this->matchesScope($owner, $scope)) { + continue; + } + + foreach ($this->normalizeRoute($route, $uri, $name, $owner) as $entry) { + $routes[] = $entry; + } + } + + // route:list 는 활성 확장만 노출한다. 비활성/미설치 확장을 명시 범위로 지정하면 + // (module:{id} / plugin:{id}) 등록된 라우트가 0건이 되므로, 그 확장의 번들 라우트 + // 파일을 프로바이더와 동일한 prefix/name 규약으로 임시 라우터에 로드해 폴백 수집한다. + if ($routes === [] && $scope !== null && $scope !== 'core' && $scope !== 'all') { + $routes = $this->collectFromBundledFiles($scope); + } + + usort($routes, fn ($a, $b) => [$a['owner']['key'], $a['uri'], $a['method']] <=> [$b['owner']['key'], $b['uri'], $b['method']]); + + return $routes; + } + + /** + * 하나의 라우트를 HTTP 메서드별 정규화 엔트리 배열로 변환합니다. + * + * @param Route $route 라우트 인스턴스 + * @param string $uri 라우트 URI ('api/' prefix 포함) + * @param string $name 라우트명 + * @param array{type: string, id: string|null, key: string} $owner 소유 주체 + * @return array> 정규화된 엔트리 목록 + */ + private function normalizeRoute(Route $route, string $uri, string $name, array $owner): array + { + $entries = []; + + $middleware = $this->gatherMiddleware($route); + + foreach ($this->httpMethods($route) as $method) { + $entries[] = [ + 'method' => $method, + 'uri' => '/'.ltrim($uri, '/'), + 'name' => $name, + 'owner' => $owner, + 'action' => $route->getActionName(), + 'controller' => $this->controllerClass($route), + 'controller_method' => $this->controllerMethod($route), + 'middleware' => $middleware, + 'permission' => $this->resolvePermission($middleware), + 'path_params' => $this->pathParams($uri), + 'path_bindings' => $this->pathBindings($route), + 'domain_group' => $this->domainGroup($name, $uri, $owner), + ]; + } + + return $entries; + } + + /** + * 라우트의 미들웨어 목록을 안전하게 수집합니다. + * + * `gatherMiddleware()` 는 컨트롤러 정의 미들웨어를 읽기 위해 컨트롤러를 인스턴스화한다. + * 비활성/미설치 확장은 서비스 바인딩이 부팅되지 않아 인스턴스화가 실패할 수 있으므로, + * 실패 시 라우트에 직접 부여된 정적 미들웨어(`middleware()`)로 폴백한다. + * + * @param Route $route 라우트 인스턴스 + * @return array 미들웨어 목록 + */ + private function gatherMiddleware(Route $route): array + { + try { + return $route->gatherMiddleware(); + } catch (\Throwable) { + return $route->middleware(); + } + } + + /** + * 비활성/미설치 확장의 번들 라우트 파일을 임시 라우터에 로드해 수집합니다. + * + * 프로바이더(`ModuleRouteServiceProvider`/`PluginRouteServiceProvider`)와 동일한 + * prefix(`api/{modules|plugins}/{id}`)·name(`api.{modules|plugins}.{id}.`)·`api` + * 미들웨어 그룹으로 `src/routes/api.php` 를 라우터에 로드한다. `api/` 로 시작하는 + * 라우트만 대상이므로 web(admin) 라우트는 자동 제외된다(그 라우트는 API 문서 대상이 아니다). + * + * 라우트 파일은 `use Illuminate\Support\Facades\Route` 로 전역 파사드에 등록하므로, + * 등록 전 라우트 이름 스냅샷을 떠서 이번 로드로 추가된 라우트만 골라낸다. 이 커맨드는 + * CLI 단발 프로세스라 전역 라우트 테이블 추가가 웹 요청에 영향을 주지 않는다. + * + * @param string $scope 범위 필터 (`module:{id}` / `plugin:{id}`) + * @return array> 정규화된 라우트 메타데이터 목록 + */ + private function collectFromBundledFiles(string $scope): array + { + if (! preg_match('/^(module|plugin):(.+)$/', $scope, $m)) { + return []; + } + + [$type, $id] = [$m[1], $m[2]]; + $dir = $type === 'module' ? 'modules' : 'plugins'; + $apiRouteFile = base_path("{$dir}/_bundled/{$id}/src/routes/api.php"); + + if (! is_file($apiRouteFile)) { + return []; + } + + $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) + ->middleware('api') + ->group($apiRouteFile); + RouteFacade::getRoutes()->refreshNameLookups(); + } catch (\Throwable) { + return []; + } + + $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/')) { + continue; + } + + $name = $route->getName() ?? ''; + $owner = $this->resolveOwner($name, $uri); + + // 폴백 대상 확장 소유로 확정되지 않으면(코어 등) 제외 + if ($owner['key'] !== $scope) { + continue; + } + + foreach ($this->normalizeRoute($route, $uri, $name, $owner) as $entry) { + $routes[] = $entry; + } + } + + return $routes; + } + + /** + * 라우트명/URI 로 소유 주체(코어/모듈/플러그인)를 판별합니다. + * + * @param string $name 라우트명 + * @param string $uri 라우트 URI + * @return array{type: string, id: string|null, key: string} 소유 주체 정보 + */ + private function resolveOwner(string $name, string $uri): array + { + if (preg_match('/^api\.modules\.([^.]+)\./', $name, $m) && $this->isRealExtensionId($m[1])) { + return ['type' => 'module', 'id' => $m[1], 'key' => 'module:'.$m[1]]; + } + + if (preg_match('/^api\.plugins\.([^.]+)\./', $name, $m) && $this->isRealExtensionId($m[1])) { + return ['type' => 'plugin', 'id' => $m[1], 'key' => 'plugin:'.$m[1]]; + } + + // 라우트명이 비어도 URI prefix 로 보조 판별 + if (preg_match('#^api/modules/([^/{]+)/#', $uri, $m) && $this->isRealExtensionId($m[1])) { + return ['type' => 'module', 'id' => $m[1], 'key' => 'module:'.$m[1]]; + } + + if (preg_match('#^api/plugins/([^/{]+)/#', $uri, $m) && $this->isRealExtensionId($m[1])) { + return ['type' => 'plugin', 'id' => $m[1], 'key' => 'plugin:'.$m[1]]; + } + + // 코어가 제공하는 확장 메타/에셋 라우트(/api/modules/{identifier}/license, + // /api/modules/bundle.js 등)는 실제 확장 소유가 아니라 코어 소유다. + return ['type' => 'core', 'id' => null, 'key' => 'core']; + } + + /** + * 세그먼트가 실제 확장 식별자인지 판별합니다 (플레이스홀더/에셋 라우트 제외). + * + * @param string $segment URI/라우트명 세그먼트 + * @return bool 실제 확장 식별자 여부 + */ + private function isRealExtensionId(string $segment): bool + { + // {identifier} 같은 라우트 파라미터, bundle/assets 같은 코어 에셋 라우트 제외. + // 실제 확장 식별자는 vendor-name 형태로 하이픈을 포함한다. + if (Str::startsWith($segment, '{') || in_array($segment, ['assets', 'bundle', 'bundle.js', 'bundle.css'], true)) { + return false; + } + + return Str::contains($segment, '-'); + } + + /** + * 소유 주체가 지정된 범위 필터에 매칭되는지 확인합니다. + * + * @param array{type: string, id: string|null, key: string} $owner 소유 주체 + * @param string $scope 범위 필터 + * @return bool 매칭 여부 + */ + private function matchesScope(array $owner, string $scope): bool + { + if ($scope === 'all') { + return true; + } + + if ($scope === 'core') { + return $owner['type'] === 'core'; + } + + return $owner['key'] === $scope; + } + + /** + * 라우트의 HTTP 메서드 목록을 반환합니다 (HEAD 제외). + * + * @param Route $route 라우트 인스턴스 + * @return array HTTP 메서드 목록 + */ + private function httpMethods(Route $route): array + { + return array_values(array_filter( + $route->methods(), + fn ($m) => $m !== 'HEAD' + )); + } + + /** + * 컨트롤러 클래스명을 반환합니다. + * + * @param Route $route 라우트 인스턴스 + * @return string|null 컨트롤러 FQCN (클로저면 null) + */ + private function controllerClass(Route $route): ?string + { + $action = $route->getActionName(); + + if ($action === 'Closure' || ! Str::contains($action, '@')) { + return null; + } + + return Str::before($action, '@'); + } + + /** + * 컨트롤러 메서드명을 반환합니다. + * + * @param Route $route 라우트 인스턴스 + * @return string|null 메서드명 (클로저면 null) + */ + private function controllerMethod(Route $route): ?string + { + $action = $route->getActionName(); + + if ($action === 'Closure' || ! Str::contains($action, '@')) { + return null; + } + + return Str::after($action, '@'); + } + + /** + * 미들웨어 목록에서 permission 식별자를 추출합니다. + * + * @param array $middleware 미들웨어 목록 + * @return string|null permission 식별자 (예: core.users.read) + */ + private function resolvePermission(array $middleware): ?string + { + foreach ($middleware as $mw) { + if (! Str::contains($mw, 'PermissionMiddleware') && ! Str::startsWith($mw, 'permission:')) { + continue; + } + + $args = Str::after($mw, ':'); + // permission:admin,core.users.read,except:self:user → core.users.read + foreach (explode(',', $args) as $part) { + $part = trim($part); + if (Str::contains($part, '.') && ! Str::startsWith($part, 'except')) { + return $part; + } + } + } + + return null; + } + + /** + * URI 의 path 파라미터 목록을 추출합니다. + * + * @param string $uri 라우트 URI + * @return array path 파라미터명 목록 + */ + private function pathParams(string $uri): array + { + preg_match_all('/\{([^}?]+)\??\}/', $uri, $m); + + return $m[1] ?? []; + } + + /** + * path 파라미터명 → 바인딩된 Eloquent 모델 클래스 맵을 추출합니다. + * + * @param Route $route 라우트 인스턴스 + * @return array 파라미터명 => 모델 FQCN + */ + private function pathBindings(Route $route): array + { + $bindings = []; + + try { + foreach ($route->signatureParameters() as $param) { + $type = $param->getType(); + + if ($type instanceof \ReflectionNamedType && ! $type->isBuiltin()) { + $class = $type->getName(); + + if (is_subclass_of($class, Model::class)) { + $bindings[$param->getName()] = $class; + } + } + } + } catch (\Throwable) { + // signatureParameters 실패 시 빈 맵 + } + + return $bindings; + } + + /** + * 문서 파일 그룹핑용 도메인 키를 산출합니다. + * + * @param string $name 라우트명 + * @param string $uri 라우트 URI + * @param array{type: string, id: string|null, key: string} $owner 소유 주체 + * @return string 도메인 키 (예: users, products) + */ + private function domainGroup(string $name, string $uri, array $owner): string + { + // 라우트명 구조: api.{context}.{resource}.{action} 또는 + // api.modules.{ext-id}.{context}.{resource}.{action} + // 도메인 = 앞쪽 연속된 context/소유 세그먼트를 걷어낸 뒤의 첫 세그먼트(리소스). + // 위치 기반이므로 'auth' 같은 리소스가 skip 리스트에 걸려 소실되지 않는다. + // 'modules'/'plugins' 는 확장 소유 prefix(api.modules.{id}.*)로도, 코어 확장관리 + // 리소스(api.admin.modules.*)로도 쓰인다. 소유 prefix 는 owner['id'] 스킵으로 이미 + // 걷히므로, leading 에는 순수 컨텍스트(api/admin/user/me/public)만 둔다. + $leading = ['api', 'admin', 'user', 'me', 'public']; + $segments = array_values(array_filter(explode('.', $name), fn ($s) => $s !== '')); + + $i = 0; + while ($i < count($segments)) { + $seg = $segments[$i]; + // 확장 소유 prefix 세그먼트(modules/plugins + ext-id)만 스킵 + $isOwnerPrefix = in_array($seg, ['modules', 'plugins'], true) && isset($segments[$i + 1]) && $segments[$i + 1] === $owner['id']; + $isLeading = in_array($seg, $leading, true) || $isOwnerPrefix || ($owner['id'] !== null && $seg === $owner['id']); + + if (! $isLeading) { + break; + } + $i++; + } + + // context(me 등) 직후가 REST 액션명이면 리소스가 아니라 그 컨텍스트가 도메인이다 + // (api.me.show → 'me', api.me.destroy → 'me'). 아니면 그 세그먼트가 리소스. + $restActions = ['show', 'index', 'store', 'update', 'destroy', 'edit', 'create']; + if (isset($segments[$i])) { + if (in_array($segments[$i], $restActions, true) && $i > 0 && in_array($segments[$i - 1], $leading, true)) { + return Str::slug($segments[$i - 1]) ?: 'misc'; + } + + return Str::slug($segments[$i]) ?: 'misc'; + } + + // 리소스 세그먼트가 없으면(context 직속) 마지막 컨텍스트를 도메인으로. + if ($i > 0 && isset($segments[$i - 1]) && in_array($segments[$i - 1], $leading, true)) { + return Str::slug($segments[$i - 1]) ?: 'misc'; + } + + // 라우트명이 비었으면 URI 에서 소유/컨텍스트 prefix 를 걷어낸 첫 리소스 세그먼트 + $parts = array_values(array_filter(explode('/', $uri), fn ($p) => $p !== '' && ! Str::startsWith($p, '{'))); + $uriLeading = ['api', 'admin', 'user', 'me', 'public']; + foreach ($parts as $p) { + if (in_array($p, $uriLeading, true) || ($owner['id'] !== null && $p === $owner['id'])) { + continue; + } + if (in_array($p, ['modules', 'plugins'], true) && ($owner['id'] !== null)) { + continue; + } + + return Str::slug($p) ?: 'misc'; + } + + return 'misc'; + } +} diff --git a/app/Support/ApiDoc/ColumnCommentResolver.php b/app/Support/ApiDoc/ColumnCommentResolver.php new file mode 100644 index 00000000..58d95edf --- /dev/null +++ b/app/Support/ApiDoc/ColumnCommentResolver.php @@ -0,0 +1,71 @@ +> 테이블별 컬럼 주석 캐시 + */ + private array $cache = []; + + /** + * 모델 테이블의 컬럼명 => 주석 맵을 반환합니다. + * + * @param class-string|null $modelClass 모델 FQCN + * @return array 컬럼명 => 주석 + */ + public function forModel(?string $modelClass): array + { + if (! $modelClass || ! class_exists($modelClass) || ! is_subclass_of($modelClass, Model::class)) { + return []; + } + + try { + $table = (new $modelClass)->getTable(); + } catch (\Throwable) { + return []; + } + + return $this->forTable($table); + } + + /** + * 테이블의 컬럼명 => 주석 맵을 반환합니다 (캐시). + * + * @param string $table 테이블명 + * @return array 컬럼명 => 주석 + */ + public function forTable(string $table): array + { + if (isset($this->cache[$table])) { + return $this->cache[$table]; + } + + $map = []; + + try { + foreach (Schema::getColumns($table) as $column) { + $comment = $column['comment'] ?? null; + + if (is_string($comment) && $comment !== '') { + $map[$column['name']] = $comment; + } + } + } catch (\Throwable) { + // 테이블 부재/드라이버 미지원 시 빈 맵 + } + + return $this->cache[$table] = $map; + } +} diff --git a/app/Support/ApiDoc/FormRequestIntrospector.php b/app/Support/ApiDoc/FormRequestIntrospector.php new file mode 100644 index 00000000..5c52cfc4 --- /dev/null +++ b/app/Support/ApiDoc/FormRequestIntrospector.php @@ -0,0 +1,200 @@ +>, hook_filters: array} + */ + public function introspect(?string $controller, ?string $method): array + { + $empty = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + + if (! $controller || ! $method || ! class_exists($controller)) { + return $empty; + } + + try { + $ref = new ReflectionMethod($controller, $method); + } catch (\ReflectionException) { + return $empty; + } + + $requestClass = $this->findFormRequestParam($ref); + + if (! $requestClass) { + return $empty; + } + + $rules = $this->extractRules($requestClass); + + return [ + 'request_class' => $requestClass, + 'params' => $this->rulesToParams($rules), + 'hook_filters' => $this->extractHookFilters($requestClass), + ]; + } + + /** + * 메서드 파라미터 중 FormRequest 하위 클래스를 찾습니다. + * + * @param ReflectionMethod $ref 메서드 리플렉션 + * @return class-string|null FormRequest FQCN + */ + private function findFormRequestParam(ReflectionMethod $ref): ?string + { + foreach ($ref->getParameters() as $param) { + $type = $param->getType(); + + if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { + continue; + } + + $class = $type->getName(); + + if (is_subclass_of($class, FormRequest::class)) { + return $class; + } + } + + return null; + } + + /** + * FormRequest 인스턴스를 만들어 rules() 를 호출합니다. + * + * @param class-string $requestClass FormRequest FQCN + * @return array 검증 규칙 배열 + */ + private function extractRules(string $requestClass): array + { + try { + $instance = new $requestClass; + + if (! method_exists($instance, 'rules')) { + return []; + } + + $rules = $instance->rules(); + + return is_array($rules) ? $rules : []; + } catch (\Throwable) { + // rules() 가 컨테이너/route 의존이면 정적 호출 실패 — 파라미터 표는 비운다. + return []; + } + } + + /** + * 검증 규칙 배열을 문서용 파라미터 메타데이터로 변환합니다. + * + * @param array $rules 검증 규칙 배열 + * @return array> 파라미터 메타데이터 목록 + */ + private function rulesToParams(array $rules): array + { + $params = []; + + foreach ($rules as $field => $rule) { + // 중첩 필드(items.*.id)는 상위만 대표로 노출 + if (str_contains((string) $field, '.')) { + continue; + } + + $tokens = is_array($rule) ? $rule : explode('|', (string) $rule); + $tokens = array_map(fn ($t) => is_string($t) ? $t : '', $tokens); + + $params[] = [ + 'name' => $field, + 'type' => $this->inferType($tokens), + 'required' => in_array('required', $tokens, true), + 'allowed' => $this->inferAllowed($tokens), + ]; + } + + return $params; + } + + /** + * 규칙 토큰에서 파라미터 타입을 유추합니다. + * + * @param array $tokens 규칙 토큰 목록 + * @return string 유추된 타입 + */ + private function inferType(array $tokens): string + { + foreach (['integer', 'numeric', 'boolean', 'array', 'string', 'date', 'email', 'file', 'image', 'uuid'] as $type) { + foreach ($tokens as $t) { + if ($t === $type || str_starts_with($t, $type)) { + return $type === 'numeric' ? 'number' : $type; + } + } + } + + return 'string'; + } + + /** + * 규칙 토큰에서 허용값(in:, max:, min: 등)을 유추합니다. + * + * @param array $tokens 규칙 토큰 목록 + * @return string 허용값 설명 (없으면 빈 문자열) + */ + private function inferAllowed(array $tokens): string + { + $parts = []; + + foreach ($tokens as $t) { + if (str_starts_with($t, 'in:')) { + $parts[] = '`'.str_replace(',', '`, `', substr($t, 3)).'`'; + } elseif (str_starts_with($t, 'max:')) { + $parts[] = 'max '.substr($t, 4); + } elseif (str_starts_with($t, 'min:')) { + $parts[] = 'min '.substr($t, 4); + } elseif (str_starts_with($t, 'between:')) { + $parts[] = 'between '.substr($t, 8); + } + } + + return implode(', ', $parts); + } + + /** + * FormRequest 소스에서 HookManager::applyFilters 훅 이름을 추출합니다. + * + * @param class-string $requestClass FormRequest FQCN + * @return array 훅 필터 이름 목록 + */ + private function extractHookFilters(string $requestClass): array + { + try { + $file = (new ReflectionClass($requestClass))->getFileName(); + + if (! $file || ! is_readable($file)) { + return []; + } + + $source = file_get_contents($file); + preg_match_all('/applyFilters\(\s*[\'"]([a-z0-9_.-]+)[\'"]/i', $source, $m); + + return array_values(array_unique($m[1] ?? [])); + } catch (\Throwable) { + return []; + } + } +} diff --git a/app/Support/ApiDoc/ParameterDescriber.php b/app/Support/ApiDoc/ParameterDescriber.php new file mode 100644 index 00000000..cf96712d --- /dev/null +++ b/app/Support/ApiDoc/ParameterDescriber.php @@ -0,0 +1,394 @@ + 정확 이름 => 설명 (위치 무관 공통 파라미터) + */ + private const EXACT = [ + // 페이지네이션 + 'page' => '조회할 페이지 번호 (1부터 시작)', + 'per_page' => '페이지당 항목 수', + 'limit' => '반환할 최대 항목 수', + 'offset' => '건너뛸 항목 수 (오프셋 페이지네이션)', + 'cursor' => '커서 기반 페이지네이션의 다음 페이지 커서', + + // 정렬 + 'sort' => '정렬 기준 (필드명, `-` 접두 시 내림차순)', + 'sort_by' => '정렬 기준 필드명', + 'sort_field' => '정렬 기준 필드명', + 'sort_direction' => '정렬 방향 (asc / desc)', + 'order_by' => '정렬 기준 필드명', + 'direction' => '정렬 방향 (asc / desc)', + // sort_order / order 는 문맥에 따라 정렬 방향(문자열 asc/desc)과 + // 표시 순서 값(정수)으로 갈리므로 EXACT 에 두지 않고 describe() 에서 + // 타입으로 분기한다. + + // 검색/필터 + 'search' => '검색어 (지정한 검색 대상 필드에서 부분 일치)', + 'q' => '검색어 (부분 일치)', + 'keyword' => '검색 키워드 (부분 일치)', + 'search_keyword' => '검색 키워드 (부분 일치)', + 'search_field' => '검색 대상 필드명 (검색어를 적용할 컬럼)', + 'search_type' => '검색 유형 (검색 대상/방식 구분)', + 'filters' => '추가 필터 조건 맵 (필드별 조건)', + 'filter' => '필터 조건', + 'start_date' => '조회 기간 시작일 (이 날짜 이후 데이터)', + 'end_date' => '조회 기간 종료일 (이 날짜 이전 데이터)', + 'date_from' => '조회 기간 시작일', + 'date_to' => '조회 기간 종료일', + 'from' => '조회 시작 값 (기간/범위 하한)', + 'to' => '조회 종료 값 (기간/범위 상한)', + 'scope' => '조회 범위 한정 키', + + // 상태 토글/플래그 + 'is_active' => '활성 여부 (true 활성 / false 비활성)', + 'is_default' => '기본값 지정 여부', + 'active' => '활성 여부', + 'published' => '발행 여부 (발행된 항목만 필터)', + 'enabled' => '사용 여부', + 'force' => '강제 실행 여부 (안전 확인/선행 검사 우회)', + 'with_trashed' => '소프트 삭제된 항목 포함 여부', + 'only_trashed' => '소프트 삭제된 항목만 조회 여부', + + // 대량 처리 + 'ids' => '대상 리소스 식별자 배열 (대량 작업 대상)', + 'items' => '처리 대상 항목 배열', + + // 국제화 + 'locale' => '로케일 코드 (표시 언어/지역)', + 'language' => '언어 코드', + 'country_code' => '국가 코드 (ISO 3166-1 alpha-2)', + 'timezone' => '타임존 식별자', + + // 인증/보안 공통 + 'password' => '비밀번호', + 'current_password' => '현재 비밀번호 (변경 전 확인용)', + 'password_confirmation' => '비밀번호 확인 (password 와 일치해야 함)', + 'token' => '인증/검증 토큰', + 'email' => '이메일 주소', + + // 주소 공통 + 'zipcode' => '우편번호', + 'address' => '기본 주소', + 'address_detail' => '상세 주소', + 'recipient_name' => '수령인 이름', + 'recipient_phone' => '수령인 연락처', + 'address_line_1' => '주소 1행 (기본 주소)', + 'address_line_2' => '주소 2행 (상세 주소)', + 'intl_city' => '도시 (국제 주소)', + 'intl_state' => '주/도 (국제 주소)', + 'intl_postal_code' => '우편번호 (국제 주소)', + 'region' => '지역/권역', + + // SEO 메타 공통 (근거: Seo\* / Page\* / Product\* FormRequest 의 + // meta_title/meta_description — 검색엔진 노출용 메타 태그 값. 도메인 무관.) + 'meta_title' => 'SEO 메타 제목 (검색엔진/소셜 공유 표시 제목)', + 'meta_description' => 'SEO 메타 설명 (검색엔진/소셜 공유 표시 요약)', + 'alt_text' => '이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구)', + + // 프로필/콘텐츠 공통 필드 (User/프로필/일반 리소스에서 의미 고정) + // 근거: User\{Create,Update}UserRequest, UpdateProfileRequest, Auth\RegisterRequest, + // Layout\* / Menu\* / Notification* / Schedule\* FormRequest + 'name' => '대상의 이름/명칭', + 'nickname' => '닉네임', + 'description' => '설명', + 'content' => '본문 내용', + 'body' => '본문', + 'subject' => '제목', + 'title' => '제목', + 'slug' => 'URL 친화 식별자 (slug)', + 'label' => '표시용 라벨', + 'phone' => '전화번호', + 'mobile' => '휴대전화 번호', + 'homepage' => '홈페이지 URL', + 'bio' => '자기소개', + 'signature' => '서명', + 'country' => '국가 코드 (ISO 3166-1 alpha-2)', + 'url' => 'URL', + 'file' => '업로드 파일', + 'files' => '업로드 파일 배열', + 'collection' => '첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default)', + 'avatar' => '아바타 이미지', + 'icon' => '아이콘', + 'value' => '값', + 'values' => '값 배열', + 'username' => '사용자명 (로그인/인증 아이디)', + 'path' => '경로', + 'data' => '데이터 페이로드', + + // 확장/버전 공통 (module/plugin/template/language-pack 설치·업데이트 계약) + // 근거: Module/Plugin/Template/LanguagePack Install·Update·Activate FormRequest, + // Extension\ChangelogRequest, Menu/Schedule/Notification* Request + 'extension_type' => '확장 유형 (core/module/plugin/template)', + 'extension_identifier' => '확장 식별자', + 'from_version' => '시작 버전 (범위 하한)', + 'to_version' => '대상 버전 (범위 상한)', + 'github_url' => 'GitHub 저장소 URL', + 'vendor' => '벤더명 (확장 제작자 식별자)', + 'vendor_mode' => '벤더 설치 모드 (auto/composer/bundled)', + 'checksum' => '무결성 검증 체크섬 (SHA-256)', + 'target_identifier' => '대상 확장 식별자', + 'source_identifier' => '출처 식별자', + 'auto_activate' => '설치 후 자동 활성화 여부', + 'cascade' => '연쇄 처리 여부 (의존 항목 함께 처리)', + 'exclude_protected' => '보호 항목 제외 여부', + + // 스케줄/작업 공통 (근거: Schedule\{Create,Update}ScheduleRequest, ScheduleListRequest) + 'command' => '실행할 아티즌 커맨드', + 'frequency' => '실행 주기', + 'priority' => '우선순위 (작을수록 우선)', + 'timeout' => '타임아웃 (초)', + 'run_in_maintenance' => '점검 모드 중 실행 여부', + 'without_overlapping' => '중복 실행 방지 여부', + 'expected_lock_version' => '낙관적 잠금 버전 (동시 편집 충돌 감지)', + + // 메일/드라이버 설정 (근거: Settings\SaveSettingsRequest, + // Settings\TestMailRequest, Settings\TestDriverConnectionRequest) + 'mailer' => '메일 발송 드라이버 (smtp/mailgun/ses)', + 'from_address' => '발신자 주소', + 'from_name' => '발신자 이름', + 'to_email' => '테스트 수신 주소', + 'host' => '호스트 주소', + 'port' => '포트 번호', + 'encryption' => '전송 암호화 방식 (tls/ssl)', + 'storage_driver' => '스토리지 드라이버 (local/s3)', + 'cache_driver' => '캐시 드라이버 (file/redis/memcached)', + 'session_driver' => '세션 드라이버 (file/database/redis)', + 'queue_driver' => '큐 드라이버 (sync/database/redis)', + 'redis_host' => 'Redis 호스트 주소', + 'redis_port' => 'Redis 포트 번호', + 'redis_password' => 'Redis 비밀번호', + 'redis_database' => 'Redis 데이터베이스 번호', + 'memcached_host' => 'Memcached 호스트 주소', + 'memcached_port' => 'Memcached 포트 번호', + 's3_bucket' => 'S3 버킷명', + 's3_region' => 'S3 리전', + 's3_access_key' => 'S3 액세스 키', + 's3_secret_key' => 'S3 시크릿 키', + 's3_url' => 'S3 엔드포인트 URL', + 'ses_key' => 'SES 액세스 키', + 'ses_secret' => 'SES 시크릿 키', + 'ses_region' => 'SES 리전', + 'mailgun_domain' => 'Mailgun 도메인', + 'mailgun_secret' => 'Mailgun 시크릿 키', + 'mailgun_endpoint' => 'Mailgun 엔드포인트', + 'websocket_enabled' => 'WebSocket 사용 여부', + 'websocket_host' => 'WebSocket 호스트 주소', + 'websocket_port' => 'WebSocket 포트 번호', + 'websocket_scheme' => 'WebSocket 스킴 (http/https)', + 'websocket_app_key' => 'WebSocket 앱 키', + ]; + + /** + * 파라미터 설명을 반환합니다. 없으면 null (호출자가 TODO 로 폴백). + * + * @param string $name 파라미터명 + * @param string $location 위치 (path/query/body) + * @param string $type 타입 (integer/string/boolean/array...) + * @return string|null 설명 (없으면 null) + */ + public function describe(string $name, string $location = '', string $type = ''): ?string + { + // sort_order / order 는 타입에 따라 의미가 갈린다: + // - 문자열: 정렬 방향(asc/desc) + // - 정수: 표시 정렬 순서 값(작을수록 우선 — 컬럼 값) + if (in_array($name, ['sort_order', 'order'], true)) { + return match ($type) { + 'integer', 'number' => '표시 정렬 순서 값 (작을수록 우선)', + 'string' => '정렬 방향 (asc 오름차순 / desc 내림차순)', + default => null, + }; + } + + // status / type / category 는 query(목록 조회)에서만 필터 의미가 고정된다. + // body(생성/수정)에서는 설정할 도메인 값이므로 의미가 도메인마다 달라 + // 사람 서술(TODO)로 남긴다. + if (in_array($name, ['status', 'type', 'category'], true)) { + if ($location !== 'query') { + return null; + } + $label = ['status' => '상태', 'type' => '유형', 'category' => '분류'][$name]; + + return "{$label} 필터 (해당 {$label}의 항목만 조회)"; + } + + if (isset(self::EXACT[$name])) { + return self::EXACT[$name]; + } + + // path 파라미터는 대부분 리소스 식별자 — 위치를 근거로 유추. + if ($location === 'path') { + return $this->describePathParam($name); + } + + return $this->byPattern($name, $type); + } + + /** + * path 파라미터(리소스 식별자)의 설명을 유추합니다. + * + * @param string $name path 파라미터명 + * @return string|null 설명 (미매칭 시 null) + */ + private function describePathParam(string $name): ?string + { + // 순수 id / *_id / *Id: 대상 리소스의 식별자 + if ($name === 'id') { + return '대상 리소스의 식별자'; + } + if (Str::endsWith($name, '_id')) { + $base = $this->humanize(Str::beforeLast($name, '_id')); + + return "대상 {$base}의 식별자"; + } + if (Str::endsWith($name, 'Id') && $name !== 'Id') { + $base = $this->humanizeCamel(Str::beforeLast($name, 'Id')); + + return "대상 {$base}의 식별자"; + } + + // slug / identifier / hash / uuid: 리소스 지시 키 + if (in_array($name, ['slug', 'identifier', 'hash', 'uuid', 'code'], true)) { + $labels = [ + 'slug' => '대상 리소스의 slug (URL 친화 식별자)', + 'identifier' => '대상 리소스의 식별자', + 'hash' => '대상 리소스의 해시 식별자', + 'uuid' => '대상 리소스의 UUID', + 'code' => '대상 리소스의 코드', + ]; + + return $labels[$name]; + } + + // *Name (templateName, pluginName, moduleName): 확장/리소스 이름 식별자 + if (Str::endsWith($name, 'Name') && $name !== 'Name') { + $base = $this->humanizeCamel(Str::beforeLast($name, 'Name')); + + return "대상 {$base}의 이름 (식별자)"; + } + + // *Identifier (templateIdentifier 등): 확장/리소스 식별자 + if (Str::endsWith($name, 'Identifier') && $name !== 'Identifier') { + $base = $this->humanizeCamel(Str::beforeLast($name, 'Identifier')); + + return "대상 {$base}의 식별자"; + } + + // bare 리소스명 path 파라미터: Laravel route-model binding 은 + // `/{user}`, `/{role}`, `/{definition}` 처럼 대상 모델의 단수형(또는 + // camelCase)을 그대로 세그먼트로 쓴다. 접미 패턴(_id/slug/*Id 등)에 + // 걸리지 않은 path 파라미터는 이 바인딩 대상 리소스의 식별자로 본다. + // 예외: key/version 은 리소스가 아니라 설정 키/버전 값이므로 EXACT 폴백. + $bareExact = [ + 'key' => '대상 설정/항목의 키', + 'version' => '대상 버전 (버전 문자열)', + ]; + if (isset($bareExact[$name])) { + return $bareExact[$name]; + } + $base = str_contains($name, '_') + ? $this->humanize($name) + : $this->humanizeCamel($name); + + return "대상 {$base}의 식별자"; + } + + /** + * query/body 파라미터의 일관된 명명 규칙으로 설명을 유추합니다. + * + * @param string $name 파라미터명 + * @param string $type 타입 + * @return string|null 설명 (미매칭 시 null) + */ + private function byPattern(string $name, string $type): ?string + { + // identifier: 확장/리소스 지시 식별자 (query/body). + // path 위치는 describePathParam 이 먼저 처리하므로 여기 도달하지 않는다. + if ($name === 'identifier') { + return '대상 확장/리소스의 식별자'; + } + + // *_name: 확장/리소스 이름 식별자 (template_name/plugin_name/module_name/layout_name 등). + // EXACT 의 recipient_name/from_name 은 여기 도달 전에 이미 처리된다. + if (Str::endsWith($name, '_name')) { + $base = $this->humanize(Str::beforeLast($name, '_name')); + + return "{$base} 이름 (식별자)"; + } + + // *_id: 연관 리소스 식별자 참조 + if (Str::endsWith($name, '_id')) { + $base = $this->humanize(Str::beforeLast($name, '_id')); + + return "{$base} 식별자"; + } + + // *_ids: 연관 리소스 식별자 배열 + if (Str::endsWith($name, '_ids')) { + $base = $this->humanize(Str::beforeLast($name, '_ids')); + + return "{$base} 식별자 배열"; + } + + // is_*/has_*: 불리언 토글 + if ((Str::startsWith($name, 'is_') || Str::startsWith($name, 'has_')) && $type === 'boolean') { + $base = $this->humanize(Str::after($name, '_')); + + return "{$base} 여부"; + } + + // *_date: 날짜 값 + if (Str::endsWith($name, '_date')) { + $base = $this->humanize(Str::beforeLast($name, '_date')); + + return "{$base} 날짜"; + } + + // *_at: 일시 값 + if (Str::endsWith($name, '_at')) { + $base = $this->humanize(Str::beforeLast($name, '_at')); + + return "{$base} 일시"; + } + + return null; + } + + /** + * snake_case 를 사람이 읽는 문구로 변환합니다. + * + * @param string $token snake_case 토큰 + * @return string 공백 구분 문구 + */ + private function humanize(string $token): string + { + return str_replace('_', ' ', $token); + } + + /** + * camelCase 를 사람이 읽는 문구로 변환합니다. + * + * @param string $token camelCase 토큰 + * @return string 공백 구분 소문자 문구 + */ + private function humanizeCamel(string $token): string + { + return Str::lower(trim(preg_replace('/([A-Z])/', ' $1', $token))); + } +} diff --git a/app/Support/ApiDoc/ResourceFieldDescriber.php b/app/Support/ApiDoc/ResourceFieldDescriber.php new file mode 100644 index 00000000..191e9254 --- /dev/null +++ b/app/Support/ApiDoc/ResourceFieldDescriber.php @@ -0,0 +1,258 @@ + 정확 필드명 => 설명 (BaseApiResource/공통 Resource 계약) + */ + private const EXACT = [ + // BaseApiResource 표준 메타 (resourceMeta) + 'is_owner' => '현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타)', + 'abilities' => '현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반)', + + // 공통 식별/타임스탬프 + 'id' => '기본 키 (내부 식별자)', + 'uuid' => '외부 노출용 UUID (URL/API 식별자, 내부 id 비노출)', + 'created_at' => '생성 일시', + 'updated_at' => '최종 수정 일시', + 'deleted_at' => '소프트 삭제 일시 (미삭제 시 null)', + + // 공통 콘텐츠/표시 필드 (도메인 무관하게 역할이 고정 — 요청 파라미터 + // ParameterDescriber 의 name/title/content/description 대응물). + // 근거: 실측 시 board 게시글·category·brand·notification 등 전 도메인에서 + // 동일 역할(명칭/제목/본문/설명)로 관측. 다국어(object)/문자열 무관. + 'name' => '대상의 이름/명칭 (다국어 필드는 로케일별 값 객체)', + 'title' => '제목', + 'content' => '본문 내용', + 'description' => '설명 (다국어 필드는 로케일별 값 객체)', + 'slug' => 'URL 친화 식별자 (slug)', + 'label' => '표시용 라벨', + 'icon' => '아이콘 식별자 (아이콘 클래스/이름)', + 'thumbnail' => '썸네일 이미지 URL/경로', + 'ip_address' => '요청/행위가 발생한 IP 주소', + + // User 계약 파생 필드 (UserResource) + 'status_label' => '상태의 사람이 읽는 라벨 (상태 Enum label() 산물)', + 'status_variant' => '상태 표시 색상/스타일 변형 키 (상태 Enum variant() 산물 — UI 배지용)', + 'language_label' => '언어 코드의 현지화 라벨 (user.language.{code} 번역)', + 'country_flag' => '국가 코드의 국기 이모지 (country 값에서 파생)', + 'country_name' => '국가 코드의 현지화 국가명 (country 값에서 파생)', + 'is_admin' => '관리자 역할 보유 여부 (User::isAdmin() — 역할 관계 기반 파생)', + + // 확장 상태 계약 필드 (ModuleResource/PluginResource/TemplateResource) + 'is_bundled' => '코어에 선탑재된 번들 확장인지 여부', + 'is_pending' => '_pending 대기소에 있어 설치 대기 중인지 여부', + 'update_available' => '최신 버전 대비 업데이트 가능 여부', + 'update_source' => '업데이트 감지 출처 (github, bundled 등)', + 'latest_version' => '감지된 최신 배포 버전', + 'file_version' => '설치된 파일의 manifest 버전', + 'incompatible_required_version' => '요구 코어 버전 미충족 시 필요한 버전 (호환되면 null)', + 'status_variant_label' => '상태 표시용 라벨/변형', + 'dependencies' => '의존하는 확장 맵 (manifest 파생 — {modules, plugins})', + 'assets' => '프론트엔드 에셋 매니페스트 (manifest 파생 — js/css 진입점·로딩 전략)', + + // 관계/연관 객체 (여러 Resource 에서 동일 계약) + 'creator' => '생성자 정보 객체 (uuid/name/email — creator 관계 파생)', + 'children' => '하위 항목 배열 (계층 트리 — children 관계 파생)', + 'parent' => '상위 항목 객체 (parent 관계 파생)', + 'permissions' => '연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생)', + 'recipient' => '수신자 사용자 객체 (uuid/name/email — recipientUser 관계 파생)', + 'sender' => '발신자 사용자 객체 (uuid/name/email — senderUser 관계 파생)', + 'author' => '작성자 사용자 객체 (uuid/name — author 관계 파생)', + 'actor_name' => '행위를 수행한 주체(사용자/시스템)의 이름', + + // 시스템/집계 공통 (DashboardService/UserRepository/SettingsService) + 'total' => '전체 개수 (집계)', + 'total_users' => '전체 사용자 수 (통계 객체는 count/추이 포함)', + 'time' => '상대 시각 표시 (예: "24초 전" — diffForHumans() 산물)', + 'server_time' => '서버 현재 시각 (Y-m-d H:i:s)', + 'number' => '목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생)', + + // 로케일 (TemplateResource/LocaleController) + 'locales' => '활성 로케일 코드 배열', + 'locale_names' => '로케일 코드별 표시명 맵 (config app.locale_names)', + + // 확장/버전 공통 (Module/Plugin/Template/LanguagePack Resource, 확장 Manager) + 'extension_type' => '이 리소스를 소유한 확장의 타입 (core/module/plugin/template)', + 'extension_identifier' => '이 리소스를 소유한 확장의 식별자', + 'extension_name' => '이 리소스를 소유한 확장의 표시 이름 (manifest name)', + 'github_url' => 'GitHub 저장소 URL (manifest 파생)', + 'github_changelog_url' => 'GitHub 변경 이력(CHANGELOG) URL (manifest 파생)', + 'changelog' => '변경 이력 텍스트 (원격/파일 CHANGELOG 본문)', + 'current_core_version' => '현재 설치된 코어 버전', + 'installed_modules' => '설치된 모듈 집계 객체 (total/active)', + 'installed_templates' => '설치된 템플릿 집계 객체 (total/active)', + 'active_plugins' => '활성 플러그인 집계 객체 (total/active)', + 'bundled_identifier' => '대응하는 번들 확장 식별자 (번들 원본 매칭용)', + 'origin' => '출처 (설치/등록 원천 구분 값)', + 'target_name' => '대상 확장의 표시 이름 (scope+target_identifier 로 해석)', + 'install_blocked_reason' => '설치가 차단된 사유 (차단 없으면 null)', + ]; + + /** + * 필드명에 대한 설명을 반환합니다. 없으면 null (호출자가 컬럼 주석/TODO 로 폴백). + * + * @param string $field 필드명 + * @param string $type 실측 타입 (boolean/integer/string/object/array...) + * @return string|null 설명 (없으면 null) + */ + public function describe(string $field, string $type = ''): ?string + { + // sort_order 는 응답에서 표시 정렬 순서 값(정수 컬럼)으로 고정된다. + // 문자열이면 정렬 방향일 수 있어 도메인 특이 → TODO 유지. + // (ParameterDescriber 의 sort_order 타입 분기와 동일 계약) + if ($field === 'sort_order') { + return in_array($type, ['integer', 'number'], true) + ? '표시 정렬 순서 값 (작을수록 우선)' + : null; + } + + if (isset(self::EXACT[$field])) { + return self::EXACT[$field]; + } + + return $this->byPattern($field, $type); + } + + /** + * 일관된 파생 규칙(접미/접두 패턴)으로 설명을 유추합니다. + * + * @param string $field 필드명 + * @param string $type 실측 타입 + * @return string|null 설명 (패턴 미매칭 시 null) + */ + private function byPattern(string $field, string $type): ?string + { + // *_at: 타임스탬프 (UI 포맷 문자열 또는 ISO) + if (Str::endsWith($field, '_at')) { + $base = $this->humanize(Str::beforeLast($field, '_at')); + + return "{$base} 일시"; + } + + // *_formatted: 원본 값을 사람이 읽는 문자열로 포맷한 표시용 파생 필드 + // (근거: size_formatted accessor, formatCurrencyPrice/formatFileSize/ + // formatCreatedAtFormat — 통화·용량·일시 등을 로케일/단위 포맷). 도메인 + // 무관하게 "`base` 값의 표시용 포맷 문자열" 로 의미가 고정된다. + if (Str::endsWith($field, '_formatted')) { + $base = Str::beforeLast($field, '_formatted'); + + return "`{$base}` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷)"; + } + + // *_label: 원본 값의 현지화 라벨 (Enum label() 또는 번역) + if (Str::endsWith($field, '_label')) { + $base = Str::beforeLast($field, '_label'); + + return "`{$base}` 값의 사람이 읽는 라벨 (현지화/Enum 파생)"; + } + + // *_variant: UI 배지 색상/스타일 변형 키 + if (Str::endsWith($field, '_variant')) { + $base = Str::beforeLast($field, '_variant'); + + return "`{$base}` 값의 표시 변형 키 (UI 배지 색상/스타일)"; + } + + // can_*: abilities 맵 내부 능력 불리언 + if (Str::startsWith($field, 'can_')) { + $action = $this->humanize(Str::after($field, 'can_')); + + return "{$action} 수행 가능 여부 (권한 기반)"; + } + + // is_*/has_*: 불리언 상태 플래그 + if ((Str::startsWith($field, 'is_') || Str::startsWith($field, 'has_')) && $type === 'boolean') { + $base = $this->humanize(Str::after($field, '_')); + + return "{$base} 여부"; + } + + // *_count: 집계 개수 + if (Str::endsWith($field, '_count') && in_array($type, ['integer', 'number'], true)) { + $base = $this->humanize(Str::beforeLast($field, '_count')); + + return "{$base} 개수 (집계)"; + } + + // *_raw: 다국어/현지화 이전 원본 값 (getValue 로 로케일 미해석 원본 반환) + // 예: name_raw = name 의 원본, description_raw = description 의 원본 + if (Str::endsWith($field, '_raw')) { + $base = Str::beforeLast($field, '_raw'); + + return "`{$base}` 의 원본 값 (현재 로케일 미해석 원본 JSON/문자열)"; + } + + // localized_* / *_localized: 다국어 필드를 현재 로케일로 해석한 표시용 값 + // (근거: getLocalizedName() / getLocalizedOptionName() 등 — 다국어 JSON 을 + // 현재 로케일 문자열로 해석). 도메인 무관 파생 규칙. + if (Str::startsWith($field, 'localized_')) { + $base = $this->humanize(Str::after($field, 'localized_')); + + return "`{$base}` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석)"; + } + if (Str::endsWith($field, '_localized')) { + $base = $this->humanize(Str::beforeLast($field, '_localized')); + + return "`{$base}` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석)"; + } + + // *_url: 리소스에 접근하는 URL (썸네일/다운로드/이미지 등) + // (근거: thumbnail_url => download_url / getThumbnailUrl()). 도메인 무관. + if (Str::endsWith($field, '_url')) { + $base = $this->humanize(Str::beforeLast($field, '_url')); + + return "{$base} URL"; + } + + // *_id: 연관 리소스를 참조하는 정수/UUID 식별자 (parent_id/user_id/loggable_id 등). + // *_ids: 연관 리소스 식별자 배열. + // 예외: login_id 는 참조 식별자가 아니라 로그인 계정 아이디(문자열)이므로 제외 + // (도메인 특이 → TODO 유지). + if (Str::endsWith($field, '_ids')) { + $base = $this->humanize(Str::beforeLast($field, '_ids')); + + return "{$base} 식별자 배열 (연관 리소스 참조)"; + } + if (Str::endsWith($field, '_id') && ! Str::endsWith($field, 'login_id')) { + $base = $this->humanize(Str::beforeLast($field, '_id')); + + return "{$base} 식별자 (연관 리소스 참조)"; + } + + // depth: 계층 트리에서의 깊이 (0 = 최상위). children/parent 트리 파생 필드로 + // 도메인 무관하게 의미가 고정된다 (근거: CommentResource/CategoryResource depth). + if ($field === 'depth' && in_array($type, ['integer', 'number'], true)) { + return '계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가)'; + } + + return null; + } + + /** + * snake_case 를 사람이 읽는 문구로 변환합니다. + * + * @param string $token snake_case 토큰 + * @return string 공백 구분 문구 + */ + private function humanize(string $token): string + { + return str_replace('_', ' ', $token); + } +} diff --git a/app/Support/ApiDoc/ResponseSchemaInferrer.php b/app/Support/ApiDoc/ResponseSchemaInferrer.php new file mode 100644 index 00000000..adb406aa --- /dev/null +++ b/app/Support/ApiDoc/ResponseSchemaInferrer.php @@ -0,0 +1,220 @@ + $body 실측 응답 body (envelope) + * @return array{envelope: array, shape: string, fields: array>, pagination: bool} + */ + public function infer(array $body): array + { + $envelope = array_keys($body); + $data = $body['data'] ?? null; + + // 목록 응답: data.data 가 배열 (BaseApiCollection) + if (is_array($data) && isset($data['data']) && is_array($data['data'])) { + $rows = array_values(array_filter($data['data'], 'is_array')); + + return [ + 'envelope' => $envelope, + 'shape' => 'collection', + 'fields' => $this->fieldsFromRows($rows), + 'pagination' => isset($data['pagination']), + ]; + } + + // 단건 응답: data 가 연관 배열 + if (is_array($data) && $this->isAssoc($data)) { + return [ + 'envelope' => $envelope, + 'shape' => 'object', + 'fields' => $this->fieldsFromRow($data), + 'pagination' => false, + ]; + } + + // data 가 순수 배열(목록만) + if (is_array($data) && ! $this->isAssoc($data) && isset($data[0]) && is_array($data[0])) { + $rows = array_values(array_filter($data, 'is_array')); + + return [ + 'envelope' => $envelope, + 'shape' => 'array', + 'fields' => $this->fieldsFromRows($rows), + 'pagination' => false, + ]; + } + + return [ + 'envelope' => $envelope, + 'shape' => 'scalar', + 'fields' => [], + 'pagination' => false, + ]; + } + + /** + * 한 행(row)에서 필드별 타입·샘플값을 추출합니다. + * + * @param array $row 응답 데이터 한 행 + * @return array> 필드 메타데이터 목록 + */ + private function fieldsFromRow(array $row): array + { + $fields = []; + + foreach ($row as $key => $value) { + $fields[] = [ + 'name' => (string) $key, + 'type' => $this->typeOf($value), + 'sample' => $this->sampleOf($value), + ]; + } + + return $fields; + } + + /** + * 여러 행을 병합해 필드별로 non-null 대표 샘플과 실제 타입을 선택합니다. + * + * 첫 행이 우연히 비어있어(null) "항상 null" 처럼 보이는 문제를 방지하기 위해, + * 각 필드에서 값이 채워진 행을 우선 채택합니다. + * + * @param array> $rows 응답 데이터 행 목록 + * @return array> 필드 메타데이터 목록 + */ + private function fieldsFromRows(array $rows): array + { + if ($rows === []) { + return []; + } + + // 첫 행의 키 순서를 기준으로 필드 목록 확정 + $keys = array_keys($rows[0]); + $fields = []; + + foreach ($keys as $key) { + $chosenValue = null; + $chosenType = 'null'; + + foreach ($rows as $row) { + if (! array_key_exists($key, $row)) { + continue; + } + + $value = $row[$key]; + $type = $this->typeOf($value); + + // 값이 채워진(non-null) 첫 행을 대표로 채택하고 탐색 종료 + if ($type !== 'null') { + $chosenValue = $value; + $chosenType = $type; + break; + } + + // 아직 non-null 을 못 찾았으면 null 이라도 후보로 유지 + $chosenValue = $value; + } + + $fields[] = [ + 'name' => (string) $key, + 'type' => $chosenType, + 'sample' => $this->sampleOf($chosenValue), + ]; + } + + return $fields; + } + + /** + * 값의 JSON 타입을 판별합니다. + * + * @param mixed $value 값 + * @return string 타입 문자열 + */ + private function typeOf(mixed $value): string + { + return match (true) { + is_bool($value) => 'boolean', + is_int($value) => 'integer', + is_float($value) => 'number', + is_string($value) => 'string', + is_null($value) => 'null', + is_array($value) && $this->isAssoc($value) => 'object', + is_array($value) => 'array', + default => 'mixed', + }; + } + + /** + * 값의 샘플 표현을 반환합니다 (문서 표에 표시할 축약형). + * + * @param mixed $value 값 + * @return string 샘플 표현 (마크다운 표 셀 안전) + */ + private function sampleOf(mixed $value): string + { + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + + if (is_null($value)) { + return 'null'; + } + + if (is_array($value)) { + $encoded = json_encode($value, JSON_UNESCAPED_UNICODE); + $encoded = (string) $encoded; + + if (mb_strlen($encoded) > 60) { + $encoded = mb_substr($encoded, 0, 57).'…'; + } + + return $this->escapeCell($encoded); + } + + $str = (string) $value; + + if (mb_strlen($str) > 40) { + $str = mb_substr($str, 0, 37).'…'; + } + + return $this->escapeCell($str); + } + + /** + * 마크다운 표 셀 안에서 안전하도록 파이프/개행을 이스케이프합니다. + * + * @param string $text 원본 텍스트 + * @return string 이스케이프된 텍스트 + */ + private function escapeCell(string $text): string + { + return str_replace(['|', "\n", "\r"], ['\\|', ' ', ''], $text); + } + + /** + * 배열이 연관 배열(맵)인지 판별합니다. + * + * @param array $arr 배열 + * @return bool 연관 배열 여부 + */ + private function isAssoc(array $arr): bool + { + if ($arr === []) { + return false; + } + + return array_keys($arr) !== range(0, count($arr) - 1); + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index a763a0de..e6ce38e6 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -3,12 +3,13 @@ namespace Database\Factories; use App\Enums\UserStatus; +use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> + * @extends Factory */ class UserFactory extends Factory { @@ -49,4 +50,37 @@ class UserFactory extends Factory 'email_verified_at' => null, ]); } + + /** + * 모든 프로필 필드가 채워진 완전한 상태를 만듭니다. + * + * API 문서 실측 시 응답 필드의 예시값이 null 이 되지 않도록, 모델 로직상 + * 유효한 값으로 nullable 프로필 컬럼을 전수 채웁니다. + * (language=지원 로케일, country=ISO alpha-2, status=UserStatus enum 등) + */ + public function complete(): static + { + return $this->state(fn (array $attributes) => [ + 'nickname' => fake()->userName(), + 'language' => 'ko', + 'timezone' => 'Asia/Seoul', + 'country' => 'KR', + 'homepage' => 'https://example.com', + 'mobile' => '010-'.fake()->numerify('####-####'), + 'phone' => '02-'.fake()->numerify('###-####'), + 'zipcode' => fake()->numerify('#####'), + 'address' => fake()->address(), + 'address_detail' => fake()->numerify('##동 ###호'), + 'signature' => fake()->sentence(), + 'bio' => fake()->paragraph(), + // avatar 는 users 테이블 컬럼이 아니라 avatarAttachment 관계에서 파생되는 + // accessor(getAvatarUrl) 이므로 factory 에서 직접 세팅하지 않는다. + 'admin_memo' => fake()->sentence(), + 'ip_address' => fake()->ipv4(), + 'last_login_at' => now()->subDays(1), + 'identity_verified_at' => now()->subDays(5), + 'mobile_verified_at' => now()->subDays(5), + 'failed_login_attempts' => 0, + ]); + } } diff --git a/docs/README.md b/docs/README.md index cda43fab..e65bf7a5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,7 +9,7 @@ | 카테고리 | 문서 수 | 링크 상태 | |----------|---------|----------| -| [백엔드](backend/) | 32개 | 정상 | +| [백엔드](backend/) | 33개 | 정상 | | [프론트엔드](frontend/) | 51개 | 정상 | | [확장 시스템](extension/) | 31개 | 정상 | | 공통 | 20개 | 정상 | @@ -113,13 +113,14 @@ ## 카테고리별 전체 문서 목록 -### 백엔드 (32개) +### 백엔드 (33개) | 문서 | 제목 | |------|------| | [activity-log-hooks.md](backend/activity-log-hooks.md) | 활동 로그 훅 레퍼런스 (Activity Log Hooks Reference) | | [activity-log.md](backend/activity-log.md) | 활동 로그 시스템 (Activity Log System) | | [admin-settings-access.md](backend/admin-settings-access.md) | Admin 환경설정 값 접근 (`g7_core_settings` vs `config()`) | +| [api-documentation.md](backend/api-documentation.md) | API 레퍼런스 문서 규정 (API Documentation) | | [api-resources.md](backend/api-resources.md) | API 리소스 | | [authentication.md](backend/authentication.md) | 인증 및 세션 처리 | | [broadcasting.md](backend/broadcasting.md) | Broadcasting (실시간 이벤트) | diff --git a/docs/backend/README.md b/docs/backend/README.md index 88a60b81..ffe7cd6d 100644 --- a/docs/backend/README.md +++ b/docs/backend/README.md @@ -23,6 +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-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 new file mode 100644 index 00000000..0dd00751 --- /dev/null +++ b/docs/backend/api-documentation.md @@ -0,0 +1,228 @@ +# API 레퍼런스 문서 규정 (API Documentation) + +> **관련 문서**: [routing.md](routing.md) | [api-resources.md](api-resources.md) | [response-helper.md](response-helper.md) | [validation.md](validation.md) + +--- + +## TL;DR (5초 요약) + +```text +1. 모든 API 엔드포인트는 레퍼런스 문서 필수 — 메서드/URI/파라미터/응답 필드 전수 기재 +2. 위치: 코어 = docs/backend/api/, 확장 = {modules|plugins}/_bundled/{id}/docs/api/ +3. 생성: php artisan api:docgen — 코드에서 추출한 스캐폴딩 + 사람이 서술 보강 (순수 수기 금지) +4. 추출 불가분(훅 주입 파라미터·동적 응답)은 마커 남기고 사람이 채움 +5. Swagger/OpenAPI 도구 미사용 — 마크다운 레퍼런스 전용 +``` + +--- + +## 목차 + +1. [왜 이 규정이 필요한가](#왜-이-규정이-필요한가) +2. [문서 위치 규칙](#문서-위치-규칙) +3. [표준 문서 포맷](#표준-문서-포맷) +4. [생성 커맨드 api:docgen](#생성-커맨드-apidocgen) +5. [문서 갱신 의무](#문서-갱신-의무) +6. [체크리스트](#체크리스트) + +--- + +## 왜 이 규정이 필요한가 + +G7 의 REST API 는 라우트 `->name()` 규약은 있으나 엔드포인트별 공개 레퍼런스가 부재했다. 프론트엔드 +(레이아웃 JSON `data_sources`)와 외부 통합 개발자가 소비하는 요청/응답 계약이 코드에만 존재해, 변경 시 +소비처가 침묵 속에서 깨진다(이슈 #64 의 `data_source` `auth_required` 계약 변화 사고가 계기). + +문서는 **코드에서 추출한 스캐폴딩 + 사람이 채운 서술의 하이브리드**로 유지한다. 674개 규모에서 완전 수기 +문서는 반드시 drift 하고, 완전 자동 추출은 훅 주입 파라미터·동적 응답을 못 잡으므로 둘 다 단독으로는 +불충분하다. + +--- + +## 문서 위치 규칙 + +| 대상 | 문서 위치 | 예시 | +|------|----------|------| +| 코어 | `docs/backend/api/{도메인}.md` | `docs/backend/api/users.md` | +| 모듈 | `modules/_bundled/{id}/docs/api/{도메인}.md` | `modules/_bundled/sirsoft-ecommerce/docs/api/products.md` | +| 플러그인 | `plugins/_bundled/{id}/docs/api/{도메인}.md` | `plugins/_bundled/sirsoft-gdpr/docs/api/consents.md` | + +확장 API 문서는 **확장이 소유**한다(코어에 모으지 않음). 확장을 배포/삭제하면 그 API 문서도 함께 이동한다. + +도메인 그룹핑은 URI/라우트명 prefix 기준(`api.admin.users.*` → `users.md`)으로 커맨드가 자동 분류한다. + +--- + +## 표준 문서 포맷 + +엔드포인트 1개당 아래 4개 구성(헤더 · 요청 파라미터 · 응답 필드 · 에러 응답)을 따른다. +`` ~ `` 사이는 `api:docgen` 이 재생성하는 추출 블록이며, +그 바깥의 사람 서술은 재생성 시 보존된다. + +에러 응답 표는 라우트 메타에서 대표 상태코드를 자동 추론한다: 인증 필수(`auth:sanctum`)→401, +`admin`/`permission:` 요구→403, FormRequest 검증 규칙 존재→422, path 파라미터 존재→404. +`optional.sanctum`(선택 인증)은 401 을 유발하지 않는다. 도메인 특이 에러(409·429 등)는 사람이 보강한다. + +```markdown +### GET /api/admin/users + +- **라우트명**: `api.admin.users.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@index` +- **인증/권한**: `auth:sanctum` + `admin` + `permission:admin,core.users.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +|------|------|------|------|--------|------| +| keyword | query | string | 아니오 | — | | +| status | query | string | 아니오 | `active`, `dormant`, `withdrawn` | | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있음 (`core.user.search_validation_rules`). + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 용도/설명 | +|------|------|-----------| +| id | integer | | +| uuid | string | | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`admin,core.users.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + +**설명** + +**응답 예시** + +​```json +{ "success": true, "data": { }, "message": null, "error": null } +​``` +``` + +### 응답 envelope 표준 + +모든 응답은 `ResponseHelper` 로 `{success, data, message, error}` 로 래핑된다(response-helper.md). +문서의 "응답 필드" 표는 이 envelope 의 `data` 내부 필드를 기재한다. + +- 목록 응답 pagination: `BaseApiCollection::paginationMeta()` → + `{current_page, last_page, per_page, total, from, to, has_more_pages}` +- 권한 메타: `BaseApiResource::resourceMeta()` → `is_owner` + `abilities.can_*` + +### 파라미터 위치 판정 + +| 위치 | 판정 근거 | +|------|----------| +| `path` | URI 의 `{param}` 세그먼트 | +| `query` | GET/DELETE 요청의 FormRequest rule | +| `body` | POST/PUT/PATCH 요청의 FormRequest rule | + +허용값은 FormRequest rule 의 `in:`, `max:`, `min:`, `Rule::in(...)`, `boolean`, `date` 등에서 유추한다. + +--- + +## 생성 커맨드 api:docgen + +```bash +# 코어 스캐폴딩 생성 (docs/backend/api/*.md) +php artisan api:docgen --scope=core + +# 특정 확장 스캐폴딩 생성 +php artisan api:docgen --scope=module:sirsoft-ecommerce +php artisan api:docgen --scope=plugin:sirsoft-gdpr + +# 전체 +php artisan api:docgen --scope=all + +# 생성 없이 누락/drift 만 리포트 (하네스가 소비) +php artisan api:docgen --check + +# 생성될 대상만 미리보기 +php artisan api:docgen --scope=core --dry-run +``` + +동작 (실측 기반): + +1. `route:list --json` 으로 API 라우트 전수 수집 (method·uri·name·middleware·action). +2. name prefix 로 소유 확장 판별 (`api.modules.{id}.*` / `api.plugins.{id}.*` / 그 외 코어) → 출력 파일 라우팅. +3. 컨트롤러 메서드의 FormRequest 타입힌트 → `rules()` 리플렉션 → 요청 파라미터 표 (타입·필수·허용값). +4. **실측**: 임시 Sanctum 토큰 발급 → 실제 요청 파라미터로 엔드포인트 호출 → **실제 응답 JSON** 관측. + - GET/HEAD: 실호출(read-only). 목록이 비면 최소 시드 데이터 자동 생성 후 재호출. + - 쓰기(POST/PUT/PATCH/DELETE): DB 트랜잭션 내 실행 후 롤백(응답 shape 만 관측, 영속 안 함). + - 외부 부수효과(결제 PG·외부 인증 콜백·메일)가 있는 라우트: allowlist 로 실호출 제외 → 정적+예시 대체. +5. 실제 응답 JSON 의 키·타입·샘플값 → 응답 필드 표 + 응답 예시. `@generated` 블록만 갱신, 사람 서술 보존. +6. 실측 후 임시 토큰·시드 데이터 정리. + +한계 / 보강: + +- FormRequest 가 `HookManager::applyFilters` 로 규칙을 주입하는 경우(163개) 정적 리플렉션은 훅 주입분을 + 못 읽는다 → 커맨드가 훅 필터 존재 시 주석을 남기고 사람이 보강. 단 **응답 필드는 실측이므로 훅으로 + 병합된 응답 필드까지 실제로 포착**된다. +- `route:list`(=`RouteFacade::getRoutes()`)는 활성 확장만 노출한다. 명시 범위(`module:{id}`/`plugin:{id}`)로 + 지정한 확장이 비활성/미설치여서 등록 라우트가 0건이면, 인벤토리가 그 확장의 번들 라우트 파일 + (`{modules|plugins}/_bundled/{id}/src/routes/api.php`)을 프로바이더와 동일한 prefix + (`api/{modules|plugins}/{id}`)·name(`api.{modules|plugins}.{id}.`)·`api` 미들웨어 규약으로 로드해 + **정적 폴백 수집**한다. 이때 실측(HTTP 호출)은 불가하므로 응답 필드는 `` + 정적 + 추정으로 대체되며, 설치 후 `--seed` 실측으로 채운다. 폴백은 `api/` 로 시작하는 라우트만 대상이므로 + web(admin) 라우트는 자동 제외된다. +- 실측이 불가한 라우트(외부 의존·allowlist 제외)는 `` 마커 + 정적 추정으로 대체. + +### 확장 실측 샘플 시더 (`--seed`) + +`--seed` 는 상세 GET 실측 시 응답 필드가 null 로 관측되는 것을 줄이기 위해, 도메인 대표 엔티티에 +완전한 샘플 레코드를 멱등 시드한다. 코어 도메인은 `App\Support\ApiDoc\ApiDocSampleService` 가 담당한다. + +확장은 자신의 도메인 샘플을 **확장이 소유**한다. `App\Contracts\ApiDoc\ApiDocSampleSeeder` 를 구현한 +클래스를 규약 위치 `{확장 네임스페이스}\Support\ApiDoc\ApiDocSampleService` +(예: `Modules\Sirsoft\Page\Support\ApiDoc\ApiDocSampleService`, 파일은 `src/Support/ApiDoc/`)에 두면, +`api:docgen --scope=module:{id} --seed` 실행 시 커맨드가 자동으로 발견해 코어 시드 뒤에 병합한다. + +- `seed()` 반환 맵의 키는 라우트 도메인 그룹명(`pages` 등), 값은 `{model, key, value}` + (모델 FQCN·route key 이름·route key 값)이다. +- 이 맵은 상세 GET 의 path 파라미터 치환에 쓰인다. 라우트-모델 바인딩이 없는 확장 패턴 + (`show(int $id)`)도, 파라미터명이 도메인의 단수 리소스명과 일치하면(`pages/{page}`) 이 맵으로 실측된다. + `{slug}`·`{hash}`·`{versionId}` 처럼 route key 가 다른 문자열/보조 파라미터는 폴백하지 않고 실측 제외된다. +- 확장에 새 PHP 클래스를 추가했으므로 `_bundled` 작업 후 `{type}:update {id} --force` 로 활성 디렉토리에 + 반영해야 오토로드된다. + +--- + +## 문서 갱신 의무 + +컨트롤러/라우트/FormRequest/Resource 를 추가·변경하면 대응 API 문서를 같은 변경 단위에서 갱신한다. + +- 트리거: `app/Http/Controllers/**`, `routes/api.php`, `app/Http/Requests/**`, `app/Http/Resources/**` + (+ 확장 대응 경로) 편집. +- 절차: 코드 변경 → `api:docgen --scope=...` 재실행 → `@generated` 블록 갱신 → 신규 TODO 서술 채움. +- 검증: `api:docgen --check` 로 drift 0 확인. audit 룰 `api-doc-coverage` 가 변경셋에 대응 문서 + 동반 여부를 검사한다. severity 는 **대상별**로 부여된다 — 문서가 완비된 대상은 `error`(문서 + 미동반 변경 차단), 진행 중 대상은 `warn`. 코어(`docs/backend/api/`)는 2026-07-08 완료로 + `error` 승격됨. 즉 코어 API 표면(`routes/api.php`·`app/Http/{Controllers,Requests,Resources}/**`)을 + 변경하면서 코어 API 문서를 함께 갱신하지 않으면 세션 종료 시 차단된다. 나머지 확장은 문서 완비 + 시 순차 승격된다(룰의 `ENFORCED_TARGETS`). + +--- + +## 체크리스트 + +```text +□ 엔드포인트가 대응 위치(코어 docs/backend/api/ 또는 확장 docs/api/)에 문서화되었는가? +□ 요청 파라미터 표에 위치/타입/필수/허용값/용도가 모두 기재되었는가? +□ 응답 필드 표가 envelope 의 data 내부 기준으로 작성되었는가? +□ 훅 주입 파라미터가 있으면 주석 + 사람 보강이 되었는가? +□ TODO 마커가 모두 채워졌는가? +□ api:docgen --check 가 drift 0 인가? +``` + +--- + +## 관련 문서 + +- [routing.md](routing.md) - 라우트 네이밍/URL 규칙 (확장 URL 스킴은 `/api/modules/{module}/...`) +- [api-resources.md](api-resources.md) - 응답 필드/pagination/abilities 형태 +- [response-helper.md](response-helper.md) - 응답 envelope 표준 +- [validation.md](validation.md) - FormRequest rule → 파라미터 허용값 유추 근거 diff --git a/docs/backend/api/activity-logs.md b/docs/backend/api/activity-logs.md new file mode 100644 index 00000000..168fa3a2 --- /dev/null +++ b/docs/backend/api/activity-logs.md @@ -0,0 +1,143 @@ +# Activity Logs API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Activity Logs 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/activity-logs + +- **라우트명**: `api.admin.activity-logs.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ActivityLogController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.activities.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| log_type | query | array | 아니오 | — | 로그 유형 필터 (원소별 값: admin 관리자, user 사용자, system 시스템 — ActivityLogType Enum). 배열로 다중 유형 동시 조회 가능 | +| action | query | string | 아니오 | max 100 | 액션 유형 필터 (예: created, updated, deleted, login — action 필드 부분/일치 검색 대상) | +| user_id | query | integer | 아니오 | — | user 식별자 | +| loggable_type | query | string | 아니오 | max 255 | 연관 리소스 모델 클래스명 필터 (예: App\Models\User — 특정 엔티티 유형의 로그만 조회) | +| search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| search_type | query | string | 아니오 | — | 검색 유형 (검색 대상/방식 구분) | +| created_by | query | string | 아니오 | max 36 | 로그를 생성한 행위 주체 식별자 필터 (행위자 기준 조회) | +| date_from | query | date | 아니오 | — | 조회 기간 시작일 | +| date_to | query | date | 아니오 | — | 조회 기간 종료일 | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| sort_by | query | string | 아니오 | — | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.activity_log.index_validation_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `87675` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `210842` | 기본 키 (내부 식별자) | +| log_type | string | `user` | 로그 유형 (admin: 관리자, user: 사용자, system: 시스템) | +| log_type_label | string | `사용자` | `log_type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| loggable_type | string | `Modules\Sirsoft\Board\Models\Attachment` | 로그가 연관된 대상 리소스의 모델 클래스 FQCN (loggable 다형성 관계 타입) | +| loggable_type_display | string | `Attachment` | `loggable_type` 의 표시용 짧은 이름 (네임스페이스 제외 클래스명 파생) | +| loggable_id | integer | `155` | loggable 식별자 (연관 리소스 참조) | +| action | string | `attachment.download` | 액션 유형 (created, updated, deleted, login, export 등) | +| action_label | string | `다운로드` | `action` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| localized_description | string | `첨부파일 다운로드 (게시물: 237)` | `description` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| description_key | string | `sirsoft-board::activity_log.descripti…` | 다국어 번역 키 (예: activity_log.description.user_create) | +| properties | object | `{"original_filename":"apidoc-sample.png","post_id":237,"c…` | 변경 상세 데이터 (old/new 값) | +| changes | object | `{"status":{"old":"inactive","new":"active","label":""}}` | 구조화된 변경 이력 (필드별 label_key, old, new, type) | +| bulk_changes | null | `null` | 일괄 수정 로그의 모델별 변경 이력 배열 (원소: model_id + changes[]). 단일 수정 로그이면 null이고 대신 changes 필드가 채워짐 | +| has_changes | boolean | `false` | changes 여부 | +| actor_name | string | `API 문서 샘플 사용자` | 행위를 수행한 주체(사용자/시스템)의 이름 | +| user | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 행위를 수행한 사용자 정보 (uuid/name/email). 시스템 발생 로그로 사용자가 없으면 name 에 "시스템" 라벨만 담김 | +| ip_address | string | `127.0.0.1` | IP 주소 (IPv6 대응) | +| created_at | string | `2026-07-07 10:00:47` | 생성 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.activities.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 시스템 활동 로그를 페이지네이션 목록으로 조회합니다. `log_type`(admin/user/system), `action`, `user_id`, `loggable_type`, 기간(`date_from`/`date_to`), 키워드(`search`) 등으로 필터링하고 `sort_by`/`sort_order`로 정렬합니다. null 값 필터는 자동으로 제외됩니다. `core.activities.read` 권한이 필요하며, 각 항목에는 현지화된 액션 라벨·변경 이력(changes)·소유자/권한 메타가 포함됩니다. 확장은 `core.activity_log.index_validation_rules` 훅으로 필터 파라미터를 추가할 수 있습니다. + + +### POST /api/admin/activity-logs/bulk-delete + +- **라우트명**: `api.admin.activity-logs.bulk-destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ActivityLogController@bulkDestroy` +- **인증/권한**: `auth:sanctum` + `permission:core.activities.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.activities.delete`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 지정한 활동 로그들을 일괄 삭제합니다. `ids` 배열에 삭제할 로그 ID를 담아 요청하며, 서비스가 각 항목을 삭제하고 실제 삭제된 건수(`deleted_count`)를 반환합니다. `core.activities.delete` 권한이 필요합니다. 로그 목록에서 여러 항목을 선택해 한 번에 정리하는 시나리오에 사용합니다. + + +### DELETE /api/admin/activity-logs/{activityLog} + +- **라우트명**: `api.admin.activity-logs.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ActivityLogController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:core.activities.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| activityLog | path | string | 예 | — | 대상 activity log의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.activities.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 단일 활동 로그를 삭제합니다. 경로의 `{activityLog}`는 라우트 모델 바인딩으로 로그 ID를 받아 해당 레코드를 삭제합니다. `core.activities.delete` 권한이 필요하며, 삭제 실패 시 오류가 로그로 기록되고 500이 반환됩니다. + + diff --git a/docs/backend/api/attachment.md b/docs/backend/api/attachment.md new file mode 100644 index 00000000..d11536c9 --- /dev/null +++ b/docs/backend/api/attachment.md @@ -0,0 +1,46 @@ +# Attachment API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Attachment 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/attachment/{hash} + +- **라우트명**: `api.attachment.download` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicAttachmentController@download` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 해시(12자)로 식별되는 첨부파일을 다운로드합니다. 이미지 파일은 캐싱 헤더와 함께 인라인으로 표시하고 그 외 파일은 다운로드 방식으로 제공합니다. 인증이 필요 없는 공개 라우트이지만 접근 권한은 AttachmentService가 로그인/비로그인 사용자 모두를 대상으로 하이브리드 방식으로 검사하며, 파일이 없으면 404, 권한이 없으면 403을 반환합니다. 게시글 첨부·상품 이미지 등 공개 리소스를 URL로 직접 내려받는 시나리오에 사용합니다. + + diff --git a/docs/backend/api/attachments.md b/docs/backend/api/attachments.md new file mode 100644 index 00000000..6b21bc66 --- /dev/null +++ b/docs/backend/api/attachments.md @@ -0,0 +1,151 @@ +# Attachments API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Attachments 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### POST /api/admin/attachments + +- **라우트명**: `api.admin.attachments.upload` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AttachmentController@upload` +- **인증/권한**: `auth:sanctum` + `permission:core.attachments.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| file | body | file | 예 | max 10240 | 업로드 파일 | +| attachmentable_type | body | string | 아니오 | max 255 | 첨부를 연결할 대상 모델의 다형성 타입 (attachmentable morph type, 예 User·Post 등 모델 클래스명). attachmentable_id와 짝을 이뤄 대상을 지정하며 미지정 시 미연결 상태로 저장 | +| attachmentable_id | body | integer | 아니오 | min 1 | attachmentable 식별자 | +| collection | body | string | 아니오 | max 100 | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) | +| source_type | body | string | 아니오 | — | 첨부 생성 출처 구분 (AttachmentSourceType Enum — core: 코어 시스템, module: 모듈, plugin: 플러그인). 미지정 시 core로 기본 설정 | +| source_identifier | body | string | 아니오 | max 255 | 출처 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.attachment.upload_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.attachments.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 단일 파일을 업로드해 첨부파일(Attachment) 레코드로 등록합니다. `attachmentable_type`/`attachmentable_id`로 대상 모델과의 다형성 연결을, `collection`으로 그룹을 지정하며 미지정 시 각각 미연결·`default` 컬렉션으로 저장됩니다. `core.attachments.create` 권한이 필요하며, 성공 시 201과 함께 생성된 첨부파일 리소스를 반환합니다. 확장은 `core.attachment.upload_validation_rules` 훅으로 검증 규칙을 추가할 수 있고, `source_type`/`source_identifier`로 업로드 출처(코어/확장)를 식별합니다. + + +### POST /api/admin/attachments/batch + +- **라우트명**: `api.admin.attachments.upload_batch` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AttachmentController@uploadBatch` +- **인증/권한**: `auth:sanctum` + `permission:core.attachments.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| files | body | array | 예 | min 1 | 업로드 파일 배열 | +| attachmentable_type | body | string | 아니오 | max 255 | 첨부를 연결할 대상 모델의 다형성 타입 (attachmentable morph type, 예 User·Post 등 모델 클래스명). attachmentable_id와 짝을 이뤄 대상을 지정하며 미지정 시 미연결 상태로 저장 | +| attachmentable_id | body | integer | 아니오 | min 1 | attachmentable 식별자 | +| collection | body | string | 아니오 | max 100 | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) | +| source_type | body | string | 아니오 | — | 첨부 생성 출처 구분 (AttachmentSourceType Enum — core: 코어 시스템, module: 모듈, plugin: 플러그인). 미지정 시 core로 기본 설정 | +| source_identifier | body | string | 아니오 | max 255 | 출처 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.attachment.upload_batch_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.attachments.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 여러 파일을 한 번의 요청으로 일괄 업로드합니다. `files` 배열의 각 파일이 개별 첨부파일 레코드로 등록되며, `attachmentable_type`/`attachmentable_id`/`collection` 등의 옵션은 배치 전체에 공통 적용됩니다. `core.attachments.create` 권한이 필요하고, 성공 시 201과 함께 생성된 첨부파일 리소스 컬렉션을 반환합니다. 갤러리·다중 이미지 첨부처럼 한 대상에 여러 파일을 붙이는 시나리오에 사용합니다. + + +### PATCH /api/admin/attachments/reorder + +- **라우트명**: `api.admin.attachments.reorder` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AttachmentController@reorder` +- **인증/권한**: `auth:sanctum` + `permission:core.attachments.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | body | array | 예 | min 1 | 재정렬 대상 목록. 각 원소는 `id`(기존 첨부파일 식별자)와 `order`(새 정렬 순서값, 0 이상 정수)를 가진 객체이며, 이 매핑대로 각 첨부의 정렬 값이 갱신됨 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.attachment.reorder_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.attachments.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 첨부파일의 표시 순서를 재정렬합니다. `order` 배열에 담긴 순서대로 각 첨부파일의 정렬 값이 갱신됩니다. `core.attachments.update` 권한이 필요합니다. 갤러리에서 드래그 앤 드롭으로 이미지 순서를 바꾸는 등 이미 등록된 첨부파일의 나열 순서만 변경할 때 사용합니다. + + +### DELETE /api/admin/attachments/{attachment} + +- **라우트명**: `api.admin.attachments.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AttachmentController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:core.attachments.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| attachment | path | string | 예 | — | 대상 attachment의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.attachments.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 지정한 첨부파일을 삭제합니다. 경로의 `{attachment}`는 라우트 모델 바인딩으로 첨부파일 ID를 받으며, 서비스가 DB 레코드와 실제 저장 파일을 함께 제거합니다. `core.attachments.delete` 권한이 필요합니다. 존재하지 않는 ID면 404가 반환됩니다. + + diff --git a/docs/backend/api/auth.md b/docs/backend/api/auth.md new file mode 100644 index 00000000..fb42d2ff --- /dev/null +++ b/docs/backend/api/auth.md @@ -0,0 +1,575 @@ +# Auth API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Auth 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### POST /api/admin/auth/logout + +- **라우트명**: `api.admin.auth.logout` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AuthController@logout` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** + +현재 관리자의 Sanctum 토큰을 폐기해 로그아웃한다. `AuthService::logout()` 이 3단계(토큰 삭제 → 세션 무효화 → `Auth::logout()`)를 수행하며, `data` 는 없고 `message` 만 `auth.logout_success` 로 내려온다. 프론트는 응답 후 저장된 Bearer 토큰을 폐기하고 로그인 화면으로 전환한다. + + +### POST /api/admin/auth/refresh + +- **라우트명**: `api.admin.auth.refresh` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AuthController@refresh` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** + +현재 관리자 토큰을 새 Sanctum 토큰으로 교체한다. `AuthService::refreshToken()` 이 기존 토큰을 폐기하고 새 토큰을 발급하며, `data` 에는 새 `token` 과 `user`(UserResource) 가 담긴다. 만료 임박 토큰을 재발급하는 용도로, 세션 만료로 재인증이 필요한 경우(토큰 무효)에는 `401 auth.unauthenticated` 를 반환한다. + + +### GET /api/admin/auth/user + +- **라우트명**: `api.admin.auth.user` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AuthController@user` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| uuid | string | `a231747f-e82e-4cf2-9ae1-a261849dce40` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) | +| name | string | `API 문서 샘플 사용자` | 사용자 이름 | +| nickname | string | `gunwoo.oh` | 닉네임 | +| email | string | `apidoc-sample-user@example.com` | 이메일 주소 | +| avatar | null | `null` | 아바타 이미지 URL (User::getAvatarUrl() — 아바타 미설정 시 null) | +| language | string | `ko` | 사용자 언어 설정 (ko: 한국어, en: 영어) | +| language_label | string | `한국어` | 언어 코드의 현지화 라벨 (user.language.{code} 번역) | +| country | string | `KR` | 국가 코드 (ISO 3166-1 alpha-2) | +| status | string | `active` | 계정 상태 (active: 활성, inactive: 비활성, blocked: 차단, withdrawn: 탈퇴) | +| 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-9070-5662` | 휴대폰 번호 | +| phone | string | `02-805-4759` | 전화번호 | +| zipcode | string | `93153` | 우편번호 | +| address | string | `대구광역시 북구 백제고분로 720` | 기본 주소 | +| address_detail | string | `40동 835호` | 상세 주소 | +| signature | string | `Ipsam rem amet expedita est.` | 서명 | +| bio | string | `Tenetur omnis et amet omnis veniam to…` | 자기소개 | +| last_login_at | string | `2026-07-05 19:15:16` | last login 일시 | +| email_verified_at | string | `2026-07-06 19:15:16` | email verified 일시 | +| timezone | string | `Asia/Seoul` | 사용자 시간대 (예: Asia/Seoul, UTC) | +| roles | array | `[{"id":1,"identifier":"admin","name":"관리자"}]` | 사용자에게 부여된 역할 목록 (원소 id/identifier/name — roles 관계 파생, name 은 현지화 라벨) | +| permissions | array | `[{"id":3,"identifier":"core.users.read","name":"사용자 조회"},…` | 연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) | +| created_at | string | `2026-07-06 19:15:16` | 생성 일시 | +| updated_at | string | `2026-07-06 19:15:16` | 최종 수정 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_create":true,"can_update":true,"can…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** + +관리자 레이아웃 전역 부트스트랩 엔드포인트. `_admin_base.json` 의 `data_source`(`current_user`)가 모든 관리자 페이지 진입 시 자동 호출해, 헤더/권한 게이트/is_admin 분기의 기준 사용자 정보를 채운다. 응답에는 `roles.permissions` 가 eager load 되어 `permissions` 배열이 함께 내려온다. + +**인증 계약**: `auth:sanctum` 필요 — Bearer 토큰이 없거나 만료되면 `401` 을 반환한다(프론트 `data_source` 의 `auth_required: true` 에 대응). 이 계약이 프론트 소비의 SSoT 이므로 미들웨어 체인 변경 시 반드시 프론트 `auth_required`/`auth_mode` 와 함께 검토한다(이슈 #64). + + +### POST /api/auth/admin/login + +- **라우트명**: `api.auth.admin.login` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AuthController@login` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| email | body | email | 예 | — | 이메일 주소 | +| password | body | string | 예 | min 6 | 비밀번호 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.login_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +관리자 로그인. `email`/`password` 검증 후 `AuthService::login()` 이 인증하고, 인증 사용자가 `isAdmin()` 이 아니면 `403 auth.admin_required` 로 거부한다. 성공 시 `data.token`(Sanctum Bearer) 과 `data.user`(UserResource) 를 반환한다. 계정 잠금 시 `AccountLockedException`, 자격 불일치 시 `422` 검증 오류를 반환한다. 이후 모든 관리자 API 호출은 이 토큰을 `Authorization: Bearer` 헤더로 실어야 한다. + + +### POST /api/auth/forgot-password + +- **라우트명**: `api.auth.forgot-password` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\AuthController@forgotPassword` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| email | body | email | 예 | — | 이메일 주소 | +| redirect_prefix | body | string | 아니오 | `admin` | 재설정 링크가 향할 화면 구분값 — `admin` 전달 시 관리자 재설정 화면, 미지정 시 사용자 화면 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.forgot_password_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +비밀번호 재설정 메일 발송을 요청한다(공개). `email` 로 계정을 찾아 재설정 링크 메일을 보내고 `message: auth.password_reset_email_sent` 를 반환한다. `redirect_prefix` 는 재설정 링크가 향할 화면을 구분하는 값으로 관리자 흐름(`admin_forgot_password.json`)에서는 `admin` 을 전달해 링크가 관리자 재설정 화면을 가리키게 한다(미지정 시 사용자 화면). 계정 열거 방지를 위해 이메일 존재 여부와 무관하게 동일 응답을 주는 것이 원칙이다. + + +### POST /api/auth/login + +- **라우트명**: `api.auth.login` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\AuthController@login` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| email | body | email | 예 | — | 이메일 주소 | +| password | body | string | 예 | min 6 | 비밀번호 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.login_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +일반 사용자 로그인(공개). 관리자 로그인과 달리 `isAdmin()` 검사가 없다. 성공 시 `data.token`(Sanctum Bearer) 과 `data.user` 를 반환하며 `message: auth.login_success`. 계정 잠금 시 `423 auth.account_locked` 를 잠금 해제까지 남은 정보와 함께 반환한다. 프론트 로그인 폼(`partials/auth/_register_form.json` 인접)에서 소비한다. + + +### POST /api/auth/logout + +- **라우트명**: `api.auth.logout` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\AuthController@logout` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** + +현재 사용자 토큰을 폐기해 로그아웃한다(`message: auth.logout_success`). 현재 요청에 사용된 토큰만 폐기하며, 모든 기기에서 로그아웃하려면 `/api/user/auth/logout-all-devices` 를 사용한다. + + +### POST /api/auth/register + +- **라우트명**: `api.auth.register` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\AuthController@register` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | string | 예 | max 255 | 대상의 이름/명칭 | +| nickname | body | string | 아니오 | max 50 | 닉네임 | +| email | body | string | 예 | max 255 | 이메일 주소 | +| password | body | string | 예 | min 8 | 비밀번호 | +| language | body | string | 아니오 | `ko`, `en`, `fr`, `ja` | 언어 코드 | +| agree_terms | body | string | 아니오 | — | 이용약관 동의 (코어 필수 동의 — accepted 규칙, 미동의 시 가입 거부) | +| agree_privacy | body | string | 아니오 | — | 개인정보 처리방침 동의 (코어 필수 동의 — accepted 규칙, 미동의 시 가입 거부) | +| agree_email_subscription | body | boolean | 아니오 | — | 광고성 이메일 수신 동의 (marketing 플러그인 주입, 선택 항목) | +| agree_marketing_consent | body | boolean | 아니오 | — | 마케팅 정보 수신 전체 동의 (marketing 플러그인 주입, 선택 항목) | +| agree_third_party_consent | body | boolean | 아니오 | — | 제3자 정보 제공 동의 (marketing 플러그인 주입, 선택 항목) | +| agree_info_disclosure | body | boolean | 아니오 | — | 개인정보 이용 안내 동의 (marketing 플러그인 주입, 선택 항목) | +| preferred_currency | body | string | 아니오 | — | 선호 결제 통화 (ecommerce 모듈 주입, 가입 시 계정 기본 통화로 저장) | +| preferred_shipping_country | body | string | 아니오 | — | 선호 배송 국가 코드 (ecommerce 모듈 주입, 가입 시 계정 기본 배송 국가로 저장) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.register_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +회원가입(공개). 성공 시 `201 auth.register_success` 와 `data.token`/`data.user` 를 반환해 가입 직후 로그인 상태로 이어진다. `agree_*` 동의 파라미터(약관/개인정보/이메일수신/마케팅/제3자제공/정보공개)는 가입 시점의 동의 이력으로 기록된다 — 그중 `agree_email_subscription`/`agree_marketing_consent`/`agree_third_party_consent`/`agree_info_disclosure` 및 `preferred_currency`/`preferred_shipping_country` 는 marketing·ecommerce 확장이 훅(`core.auth.register_validation_rules`)으로 주입하는 파라미터로, 해당 확장 비활성 시 무시된다. 검증 실패 시 `422 auth.register_failed`. + + +### POST /api/auth/reset-password + +- **라우트명**: `api.auth.reset-password` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\AuthController@resetPassword` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| token | body | string | 예 | — | 인증/검증 토큰 | +| email | body | email | 예 | — | 이메일 주소 | +| password | body | string | 예 | min 8 | 비밀번호 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.reset_password_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +비밀번호 재설정을 실제 반영한다(공개). 재설정 메일의 `token` 과 `email`, 새 `password` 를 받아 비밀번호를 갱신하고 `message: auth.password_reset_success`. 토큰 만료/불일치 등 검증 실패 시 `422 auth.password_reset_failed`. 반영 전 토큰 유효성만 먼저 확인하려면 `/api/auth/validate-reset-token` 을 사용한다. + + +### GET /api/auth/user + +- **라우트명**: `api.auth.user` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\AuthController@user` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| uuid | string | `a231747f-e82e-4cf2-9ae1-a261849dce40` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) | +| name | string | `API 문서 샘플 사용자` | 사용자 이름 | +| nickname | string | `gunwoo.oh` | 닉네임 | +| email | string | `apidoc-sample-user@example.com` | 이메일 주소 | +| avatar | null | `null` | 아바타 이미지 URL (User::getAvatarUrl() — 아바타 미설정 시 null) | +| language | string | `ko` | 사용자 언어 설정 (ko: 한국어, en: 영어) | +| language_label | string | `한국어` | 언어 코드의 현지화 라벨 (user.language.{code} 번역) | +| country | string | `KR` | 국가 코드 (ISO 3166-1 alpha-2) | +| status | string | `active` | 계정 상태 (active: 활성, inactive: 비활성, blocked: 차단, withdrawn: 탈퇴) | +| 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-9070-5662` | 휴대폰 번호 | +| phone | string | `02-805-4759` | 전화번호 | +| zipcode | string | `93153` | 우편번호 | +| address | string | `대구광역시 북구 백제고분로 720` | 기본 주소 | +| address_detail | string | `40동 835호` | 상세 주소 | +| signature | string | `Ipsam rem amet expedita est.` | 서명 | +| bio | string | `Tenetur omnis et amet omnis veniam to…` | 자기소개 | +| last_login_at | string | `2026-07-05 19:15:16` | last login 일시 | +| email_verified_at | string | `2026-07-06 19:15:16` | email verified 일시 | +| timezone | string | `Asia/Seoul` | 사용자 시간대 (예: Asia/Seoul, UTC) | +| modules_count | array | `[]` | 접근 가능 모듈 수 (modules_count 속성이 로드된 경우에만 포함 — whenLoaded 성격의 조건부 필드) | +| plugins_count | array | `[]` | 접근 가능 플러그인 수 (plugins_count 속성이 로드된 경우에만 포함) | +| menus_count | array | `[]` | 접근 가능 메뉴 수 (menus_count 속성이 로드된 경우에만 포함) | +| modules | array | `[]` | 접근 가능 모듈 목록 (원소 id/name/slug/is_active — modules 관계 로드 시에만 포함) | +| plugins | array | `[]` | 접근 가능 플러그인 목록 (원소 id/name/slug/is_active — plugins 관계 로드 시에만 포함) | +| menus | array | `[]` | 접근 가능 메뉴 목록 (원소 id/title/url/is_active — menus 관계 로드 시에만 포함) | +| roles | array | `[{"id":1,"identifier":"admin","name":"관리자"}]` | 사용자에게 부여된 역할 목록 (원소 id/identifier/name — roles 관계 파생, name 은 현지화 라벨) | +| permissions | array | `[{"id":3,"identifier":"core.users.read","name":"사용자 조회"},…` | 연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) | +| consents | array | `[]` | 전체 약관 동의 이력 (원소 consent_type/agreed_at/revoked_at — consents 관계 로드 시 포함, 플러그인 참조용) | +| terms_consent | array | `[]` | 이용약관 동의 정보 (agreed_at — ConsentType::Terms 동의 이력에서 파생, 미동의 시 null) | +| privacy_consent | array | `[]` | 개인정보 처리방침 동의 정보 (agreed_at — ConsentType::Privacy 동의 이력에서 파생, 미동의 시 null) | +| created_at | string | `2026-07-06 19:15:16` | 생성 일시 | +| updated_at | string | `2026-07-06 19:15:16` | 최종 수정 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_create":true,"can_update":true,"can…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +| notify_post_complete | boolean | `false` | 게시판 새 글 작성 완료 알림 수신 설정 (marketing 플러그인 주입) | +| notify_post_reply | boolean | `false` | 내 게시글에 답글 달림 알림 수신 설정 (marketing 플러그인 주입) | +| notify_comment | boolean | `false` | 내 게시글에 댓글 달림 알림 수신 설정 (marketing 플러그인 주입) | +| notify_reply_comment | boolean | `false` | 내 댓글에 답글 달림 알림 수신 설정 (marketing 플러그인 주입) | +| email_subscription | boolean | `false` | 광고성 이메일 수신 동의 여부 (marketing 플러그인 주입) | +| email_subscription_at | null | `null` | email subscription 일시 (광고성 이메일 수신 동의 시각, 미동의 시 null) | +| marketing_consent | boolean | `false` | 마케팅 정보 수신 전체 동의 마스터 키 (marketing 플러그인 주입) | +| marketing_consent_at | null | `null` | marketing consent 일시 (마케팅 정보 수신 동의 시각, 미동의 시 null) | +| third_party_consent | boolean | `false` | 제3자 정보 제공 동의 여부 (법적 항목 — marketing 플러그인 주입) | +| third_party_consent_at | null | `null` | third party consent 일시 (제3자 정보 제공 동의 시각, 미동의 시 null) | +| info_disclosure | boolean | `false` | 개인정보 이용 안내 동의 여부 (법적 항목 — marketing 플러그인 주입) | +| info_disclosure_at | null | `null` | info disclosure 일시 (개인정보 이용 안내 동의 시각, 미동의 시 null) | +| marketing_consent_enabled | boolean | `true` | 마케팅 정보 수신 동의 항목 UI 노출 여부 (관리자 활성화 플래그) | +| marketing_consent_terms_slug | string | `marketing-terms` | 마케팅 정보 수신 동의에 연결된 약관 slug (미설정 시 null) | +| marketing_consent_terms_slug_set | boolean | `true` | 마케팅 정보 수신 약관 연결 존재 여부 (프론트 링크 표시 판정용) | +| third_party_consent_enabled | boolean | `true` | 제3자 정보 제공 동의 항목 UI 노출 여부 (관리자 활성화 플래그) | +| third_party_consent_terms_slug | null | `null` | 제3자 정보 제공 동의에 연결된 약관 slug (미설정 시 null) | +| third_party_consent_terms_slug_set | boolean | `false` | 제3자 정보 제공 약관 연결 존재 여부 (프론트 링크 표시 판정용) | +| info_disclosure_enabled | boolean | `true` | 개인정보 이용 안내 동의 항목 UI 노출 여부 (관리자 활성화 플래그) | +| info_disclosure_terms_slug | null | `null` | 개인정보 이용 안내 동의에 연결된 약관 slug (미설정 시 null) | +| info_disclosure_terms_slug_set | boolean | `false` | 개인정보 이용 안내 약관 연결 존재 여부 (프론트 링크 표시 판정용) | +| email_subscription_enabled | boolean | `true` | 광고성 이메일 수신 동의 항목 UI 노출 여부 (관리자 활성화 플래그) | +| email_subscription_terms_slug | null | `null` | 광고성 이메일 수신 동의에 연결된 약관 slug (미설정 시 null) | +| email_subscription_terms_slug_set | boolean | `false` | 광고성 이메일 수신 약관 연결 존재 여부 (프론트 링크 표시 판정용) | +| channels | array | `[{"key":"email_subscription","label":"광고성 이메일 수신","enable…` | 관리자 정의 전체 마케팅 채널 목록 (원소 key/label/enabled/terms_slug — marketing 플러그인 주입) | +| consent_histories | array | `[]` | 동의 변경 이력 (원소 channel_key/action/source/created_at — marketing 플러그인 주입) | +| ecommerce_mileage | object | `{"enabled":false}` | 마일리지 정보 (enabled/잔액 — ecommerce 모듈 주입, 모듈 비활성 시 enabled=false) | +| ecommerce_preferred_currency | null | `null` | 선호 결제 통화 (ecommerce 모듈 주입, 미설정 시 null) | +| ecommerce_preferred_shipping_country | null | `null` | 선호 배송 국가 코드 (ecommerce 모듈 주입, 미설정 시 null) | +| ecommerce_preferred_shipping_country_name | null | `null` | 선호 배송 국가 이름 (국가 코드에서 현지화 파생 — ecommerce 모듈 주입, 미설정 시 null) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** + +프론트(사용자) 레이아웃 전역 부트스트랩 엔드포인트. `_user_base.json` 의 `current_user` data_source 가 모든 페이지 진입 시 호출한다. 관리자 `user` 와 달리 응답을 `UserResource::toAuthArray()` 로 만들어 `core.user.filter_resource_data` 필터를 적용하므로, marketing 플러그인·ecommerce 모듈이 훅으로 병합한 필드(`notify_*`, `marketing_consent*`, `channels`, `ecommerce_*` 등)가 함께 내려온다. 이 필드들은 확장 소유이므로 상세 설명은 각 확장 문서를 따른다. 로그인 시 이 응답이 계정 영속 통화를 덮어쓰는 계약(D-LOGIN-CUR)의 출처다. + +**인증 계약**: 이 경로(`api.auth.user`)는 `auth:sanctum` 으로 인증이 필수다. 인증 여부와 무관하게 게스트 컨텍스트가 필요한 화면은 `optional.sanctum` 이 걸린 `/api/user/auth/user`(`api.user.auth.user`)를 사용해야 한다 — 프론트 `data_source` 의 `auth_mode: "optional"` 이 이 경로에 대응한다. 두 경로의 미들웨어 차이가 곧 `auth_required`/`auth_mode` 계약이므로 변경 시 프론트와 함께 검토한다(이슈 #64). + + +### POST /api/auth/validate-reset-token + +- **라우트명**: `api.auth.validate-reset-token` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\AuthController@validateResetToken` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| token | body | string | 예 | — | 인증/검증 토큰 | +| email | body | email | 예 | — | 이메일 주소 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.validate_reset_token_rules`, `core.auth.validate_reset_token_messages`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +비밀번호 재설정 토큰의 유효성만 사전 확인한다(공개, 비밀번호 미변경). 재설정 화면(`admin_reset_password.json`/`auth/reset_password.json`) 진입 시 토큰/이메일이 유효한지 먼저 검사해, 만료·위조 링크면 즉시 오류 화면을 보이고 유효하면 새 비밀번호 입력 폼을 노출하는 용도다. 실제 반영은 `/api/auth/reset-password` 가 담당한다. + + +### POST /api/user/auth/logout + +- **라우트명**: `api.user.auth.logout` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\AuthController@logout` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.auth.logout` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.auth.logout`)이 없는 경우 | + + + +**설명** + +`/user` prefix 그룹의 사용자 로그아웃. 공용 경로 `/api/auth/logout` 과 동일하게 현재 토큰을 폐기하되, `permission:core.auth.logout` 권한 게이트를 추가로 통과해야 한다. 세션 시작(`start.api.session`)이 걸린 공용 경로와 달리 권한 기반 접근 제어가 필요한 흐름에서 사용한다. + + +### POST /api/user/auth/logout-all-devices + +- **라우트명**: `api.user.auth.logout-all-devices` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\AuthController@logoutFromAllDevices` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.auth.logout` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.auth.logout`)이 없는 경우 | + + + +**설명** + +현재 사용자의 **모든** Sanctum 토큰을 폐기해 전 기기에서 로그아웃한다(`message: auth.logout_all_devices_success`). 비밀번호 변경 후 기존 세션 무효화, 계정 도용 대응 등에 사용한다. + + +### POST /api/user/auth/refresh + +- **라우트명**: `api.user.auth.refresh` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\AuthController@refresh` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.auth.refresh` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.auth.refresh`)이 없는 경우 | + + + +**설명** + +`/user` prefix 그룹의 토큰 갱신. 공용 `refresh` 와 동작은 같으나 `permission:core.auth.refresh` 권한 게이트를 추가로 통과해야 한다. 이 그룹(`routes/api.php:271`)은 `optional.sanctum` + `RefreshTokenExpiration` 미들웨어 아래 있어 토큰 만료 정책 갱신과 함께 동작한다. + + +### GET /api/user/auth/user + +- **라우트명**: `api.user.auth.user` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\AuthController@user` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.auth.user` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.auth.user`)이 없는 경우 | + + + +**설명** + +`/user` prefix 그룹(`routes/api.php:271`)의 현재 사용자 정보. 이 그룹은 `optional.sanctum` 미들웨어 아래 있어 **비인증(게스트) 요청도 통과**하며, 게스트 컨텍스트가 필요한 프론트 화면의 `data_source`(`auth_mode: "optional"`)가 이 경로를 소비한다. 응답 필드는 인증된 경우 공용 `/api/auth/user` 와 동일 형태(`toAuthArray` 병합 포함)이며, 이 경로에는 추가로 `permission:core.auth.user` 권한 게이트가 걸린다. 실측이 `403` 으로 제외된 것은 샘플 사용자에 해당 권한이 없었기 때문으로, 응답 shape 은 공용 `user` 경로를 참조한다. + +> **인증 계약 요약(이슈 #64)**: `api.auth.user`(`auth:sanctum`, 필수) ↔ `api.user.auth.user`(`optional.sanctum`, 선택). 프론트 `data_source` 의 `auth_required: true` 는 전자에, `auth_mode: "optional"` 은 후자에 대응한다. 어느 한쪽 미들웨어를 바꾸면 프론트 소비 계약이 침묵 속에서 깨지므로 반드시 양쪽 문서를 함께 갱신한다. + + diff --git a/docs/backend/api/avatar.md b/docs/backend/api/avatar.md new file mode 100644 index 00000000..e36c90e0 --- /dev/null +++ b/docs/backend/api/avatar.md @@ -0,0 +1,74 @@ +# Avatar API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Avatar 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### DELETE /api/me/avatar + +- **라우트명**: `api.me.avatar.delete` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@deleteAvatar` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 현재 인증 사용자의 아바타를 삭제합니다. 사용자에 연결된 아바타 첨부파일(Attachment) 레코드와 실제 파일을 함께 제거하며, 삭제 활동이 로그로 기록됩니다. 아바타가 없으면 404(`user.avatar_not_found`)를 반환합니다. `auth:sanctum` 인증만 필요하고 별도 권한은 없으며, 사용자가 자신의 프로필 사진을 기본값으로 되돌리는 시나리오에 사용합니다. + + +### POST /api/me/avatar + +- **라우트명**: `api.me.avatar.upload` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@uploadAvatar` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| avatar | body | image | 예 | max 2048 | 아바타 이미지 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.upload_avatar_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 현재 인증 사용자의 아바타 이미지를 업로드합니다. 기존 아바타가 있으면 먼저 삭제한 뒤 새 이미지를 `avatar` 컬렉션의 다형성 첨부파일로 등록하고, 업로드 활동을 로그로 기록합니다. `auth:sanctum` 인증만 필요하고 별도 권한은 없으며, 이미지는 최대 2048KB로 제한됩니다. 확장은 `core.user.upload_avatar_rules` 훅으로 검증 규칙을 추가할 수 있습니다. + + diff --git a/docs/backend/api/broadcasting.md b/docs/backend/api/broadcasting.md new file mode 100644 index 00000000..81ef8951 --- /dev/null +++ b/docs/backend/api/broadcasting.md @@ -0,0 +1,43 @@ +# Broadcasting API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Broadcasting 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### POST /api/broadcasting/auth + +- **라우트명**: `api.broadcasting.auth` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** WebSocket 프라이빗/프레즌스 채널 구독 시 Laravel Broadcast 채널 인증을 수행하는 엔드포인트입니다. `auth:sanctum` 토큰으로 인증하며, 웹소켓 사용이 OFF(`broadcasting.default === 'null'`)이면 채널 인증을 거부해 403을 반환합니다(reverb.key 무력화를 우회한 직접 연결 시도까지 차단). 컨트롤러 없이 라우트 클로저가 토글 가드를 적용한 뒤 `Broadcast::auth`에 위임하며, 실시간 이벤트 구독을 위해 클라이언트 브로드캐스팅 라이브러리가 자동 호출합니다. + + diff --git a/docs/backend/api/changelog.md b/docs/backend/api/changelog.md new file mode 100644 index 00000000..f078e7fa --- /dev/null +++ b/docs/backend/api/changelog.md @@ -0,0 +1,48 @@ +# Changelog API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Changelog 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/changelog + +- **라우트명**: `api.admin.changelog` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LicenseController@changelog` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| content | string | `# Changelog 이 프로젝트의 모든 주요 변경사항을 기록합니…` | 본문 내용 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 코어의 `CHANGELOG.md` 파일 원문 텍스트를 `content`로 반환합니다. `auth:sanctum` 인증이 필요하며, 파일이 없으면 404(`common.not_found`)를 반환합니다. 관리자 화면에서 코어의 전체 변경 이력을 마크다운 원문 그대로 표시하는 용도로 사용합니다. + + diff --git a/docs/backend/api/core-update.md b/docs/backend/api/core-update.md new file mode 100644 index 00000000..cdc5e984 --- /dev/null +++ b/docs/backend/api/core-update.md @@ -0,0 +1,91 @@ +# Core Update API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Core Update 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/core-update/changelog + +- **라우트명**: `api.admin.core-update.changelog` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\CoreUpdateController@changelog` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| source | query | string | 아니오 | `active`, `bundled`, `github` | 어느 위치의 CHANGELOG를 조회할지 지정 (active: 활성 설치본, bundled: 번들 원본, github: 원격 릴리스). 미지정 시 기본 조회 경로를 사용 | +| from_version | query | string | 아니오 | — | 시작 버전 (범위 하한) | +| to_version | query | string | 아니오 | — | 대상 버전 (범위 상한) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.extension.changelog_rules`). + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| changelog | array | `[{"version":"7.0.2","date":"2026-07-05","categories":[{"n…` | 변경 이력 텍스트 (원격/파일 CHANGELOG 본문) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 코어의 버전별 변경사항(CHANGELOG)을 구조화된 배열로 조회합니다. `source`(active/bundled/github)로 어느 위치의 CHANGELOG를 읽을지, `from_version`/`to_version`으로 조회 범위를 지정합니다. `core.settings.read` 권한이 필요하며, 업데이트 안내 화면에서 새 버전에 무엇이 바뀌는지 보여줄 때 사용합니다. 확장은 `core.extension.changelog_rules` 훅으로 파라미터를 확장할 수 있습니다. + + +### POST /api/admin/core-update/check + +- **라우트명**: `api.admin.core-update.check` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\CoreUpdateController@checkForUpdates` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 용도/설명 | +| --- | --- | --- | +| update_available | boolean | 업데이트 가능 여부 (최신 버전이 현재 버전보다 높으면 true) | +| current_version | string | 현재 설치된 코어 버전 | +| latest_version | string | GitHub 릴리스에서 확인한 최신 버전 (조회 실패 시 현재 버전으로 대체) | +| github_url | string | 버전 확인 대상 GitHub 저장소 URL (`config('app.update.github_url')`) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | + + + +**설명** GitHub 릴리스를 기준으로 코어 업데이트 가능 여부를 확인합니다. 현재 버전과 최신 버전을 비교한 결과를 반환하며, 조회에 실패하면 실패 사유·현재 버전·github_url과 함께 422를 반환합니다. `core.settings.update` 권한이 필요하고, 관리자가 업데이트 확인 버튼을 눌러 새 버전 유무를 점검하는 시나리오에 사용합니다. + + diff --git a/docs/backend/api/dashboard.md b/docs/backend/api/dashboard.md new file mode 100644 index 00000000..fd2b5d65 --- /dev/null +++ b/docs/backend/api/dashboard.md @@ -0,0 +1,183 @@ +# Dashboard API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Dashboard 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/dashboard/activities + +- **라우트명**: `api.admin.dashboard.activities` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\DashboardController@activities` +- **인증/권한**: `auth:sanctum` + `permission:core.dashboard.activities` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| type | string | `user` | 활동 분류 (log_type Enum 값 — admin: 관리자, user: 사용자, system: 시스템) | +| icon | string | `circle-info` | 아이콘 식별자 (아이콘 클래스/이름) | +| icon_color | string | `green` | 분류별 색상 (log_type Enum variant() 파생 — admin: blue, user: green, system: gray) | +| title | string | `첨부파일 다운로드 (게시물: 237)` | 제목 | +| description | string | `API 문서 샘플 사용자` | 설명 (다국어 필드는 로케일별 값 객체) | +| time | string | `4시간 전` | 상대 시각 표시 (예: "24초 전" — diffForHumans() 산물) | +| timestamp | string | `2026-07-07T10:00:47+09:00` | 활동 발생 절대 시각 (created_at 을 사용자 타임존으로 변환한 ISO 8601) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.dashboard.activities`)이 없는 경우 | + + + +**설명** 관리자 대시보드에 표시할 최근 활동 내역(사용자 등록, 모듈 활성화 등)을 조회합니다. 인증(`auth:sanctum`)과 `core.dashboard.activities` 권한이 필요합니다. 각 항목은 유형·아이콘·제목·설명과 상대 시간(`time`)·절대 시각(`timestamp`)을 포함하며, 대시보드 최근 활동 카드를 렌더링할 때 사용합니다. + + +### GET /api/admin/dashboard/alerts + +- **라우트명**: `api.admin.dashboard.alerts` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\DashboardController@alerts` +- **인증/권한**: `auth:sanctum` + `permission:core.dashboard.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.dashboard.read`)이 없는 경우 | + + + +**설명** 시스템 업데이트·경고 등 관리자에게 알릴 시스템 알림 목록을 조회합니다. 인증(`auth:sanctum`)과 `core.dashboard.read` 권한이 필요합니다. 알릴 항목이 없으면 빈 목록을 반환하며(위 실측이 빈 상태였던 이유), 대시보드 상단 시스템 알림 영역을 렌더링할 때 사용합니다. + + +### GET /api/admin/dashboard/recent-notifications + +- **라우트명**: `api.admin.dashboard.recent-notifications` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\DashboardController@recentNotifications` +- **인증/권한**: `auth:sanctum` + `permission:core.notification-logs.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `677` | 기본 키 (내부 식별자) | +| type | string | `apidoc.sample.event` | 알림 유형 식별자 (notification_type — 발송을 유발한 알림 정의 키) | +| channel | string | `mail` | 발송 채널 (mail: 이메일, database: 인앱, sms 등 알림이 전달된 매체) | +| recipient | string | `API 문서 샘플 사용자` | 수신자 표시명 (recipientUser 관계의 name → recipient_name → recipient_identifier 순 폴백) | +| subject | string | `API 문서 샘플 알림` | 알림 제목 (subject 를 50자로 절삭한 값) | +| status | string | `sent` | 발송 상태 (status Enum 값 — sent: 발송 성공, failed: 발송 실패, skipped: 발송 건너뜀) | +| time | string | `19시간 전` | 상대 시각 표시 (예: "24초 전" — diffForHumans() 산물) | +| timestamp | string | `2026-07-06T18:20:23+09:00` | 발송 절대 시각 (sent_at, 없으면 created_at 을 사용자 타임존으로 변환한 ISO 8601) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.notification-logs.read`)이 없는 경우 | + + + +**설명** 대시보드 "최근 알림" 카드에 표시할 최근 알림 발송 이력을 조회합니다. 인증(`auth:sanctum`)과 `core.notification-logs.read` 권한이 필요합니다. 각 항목은 알림 타입·채널·수신자·제목·상태와 상대 시간(`time`)·절대 시각(`timestamp`)을 포함하며, 전체 이력 목록(notification-logs)의 요약 뷰를 대시보드에 노출할 때 사용합니다. + + +### GET /api/admin/dashboard/resources + +- **라우트명**: `api.admin.dashboard.resources` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\DashboardController@resources` +- **인증/권한**: `auth:sanctum` + `permission:core.dashboard.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| cpu | object | `{"percentage":6,"color":"green"}` | CPU 사용률 정보 (percentage: 0~100 사용률, color: 임계 색상 — green<50, blue 50~69, yellow 70~89, red≥90) | +| 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 폴백 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.dashboard.read`)이 없는 경우 | + + + +**설명** 서버의 CPU·메모리·디스크 사용량을 조회합니다. 인증(`auth:sanctum`)과 `core.dashboard.read` 권한이 필요합니다. 각 항목은 사용률(`percentage`)과 상태 색상(`color`), 메모리·디스크의 경우 사용량/총량 문자열을 포함하며, 대시보드 시스템 리소스 게이지를 렌더링할 때 사용합니다. + + +### GET /api/admin/dashboard/stats + +- **라우트명**: `api.admin.dashboard.stats` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\DashboardController@stats` +- **인증/권한**: `auth:sanctum` + `permission:core.dashboard.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| total_users | object | `{"count":156,"change_percent":15500,"change_display":"+15…` | 전체 사용자 수 (통계 객체는 count/추이 포함) | +| installed_modules | object | `{"total":3,"active":3}` | 설치된 모듈 집계 객체 (total/active) | +| active_plugins | object | `{"total":9,"active":9}` | 활성 플러그인 집계 객체 (total/active) | +| installed_templates | object | `{"total":2,"active":2}` | 설치된 템플릿 집계 객체 (total/active) | +| 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: 전체 서비스 정상 동작 여부) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.dashboard.read`)이 없는 경우 | + + + +**설명** 대시보드 상단 통계 카드에 표시할 집계 데이터를 조회합니다. 인증(`auth:sanctum`)과 `core.dashboard.read` 권한이 필요합니다. 총 사용자 수(증감률 포함), 설치/활성 모듈·플러그인·템플릿·언어팩 수, 시스템 상태를 객체 형태로 반환하며, 대시보드 진입 시 요약 지표를 렌더링할 때 사용합니다. + + diff --git a/docs/backend/api/extensions.md b/docs/backend/api/extensions.md new file mode 100644 index 00000000..3b1cf1f5 --- /dev/null +++ b/docs/backend/api/extensions.md @@ -0,0 +1,110 @@ +# Extensions API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Extensions 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/extensions/auto-deactivated + +- **라우트명**: `api.admin.extensions.auto-deactivated` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ExtensionRecoveryController@autoDeactivated` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.activate` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| items | object | `{"plugins":[],"modules":[],"templates":[]}` | 코어 비호환으로 자동 비활성화된 확장을 타입별(`plugins`/`modules`/`templates`)로 묶은 목록. 각 원소는 식별자(`identifier`), 비호환 요구 버전(`incompatible_required_version`), 비활성화 시각(`deactivated_at`)을 가지며, 사용자가 dismiss했거나 hidden(학습용 샘플) 확장은 제외됨 | +| current_core_version | string | `7.0.1` | 현재 설치된 코어 버전 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.activate`)이 없는 경우 | + + + +**설명** 코어 버전 비호환으로 자동 비활성화된 확장 목록을 타입별(`plugins`/`modules`/`templates`)로 반환합니다. 각 항목에는 식별자, 비호환 요구 버전, 비활성화 시각과 함께 현재 코어 버전이 담깁니다. 사용자가 dismiss한 알림과 hidden(학습용 샘플) 확장은 결과에서 제외됩니다. `core.plugins.activate` 권한이 필요하며, 상단 배너·대시보드 카드의 데이터 소스로 사용됩니다. + + +### POST /api/admin/extensions/{type}/{identifier}/dismiss + +- **라우트명**: `api.admin.extensions.dismiss` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ExtensionRecoveryController@dismiss` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.activate` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| type | path | string | 예 | module, plugin, template | 대상 확장의 타입 (module: 모듈, plugin: 플러그인, template: 템플릿). 타입에 맞는 Repository/Manager를 해석하는 데 사용되며 그 외 값은 422 | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.activate`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 확장의 호환성 알림을 현재 사용자 기준으로 dismiss(닫기) 처리합니다. 경로의 `{type}`(module|plugin|template)과 `{identifier}`로 대상을 지정하며, 해당 확장의 자동 비활성화 알림과 재호환 알림을 함께 dismiss합니다. `core.plugins.activate` 권한이 필요합니다. dismiss는 사용자별로 저장되므로, 캐시 만료나 감지 갱신 시 재호환 상태가 바뀌면 다시 노출될 수 있습니다. + + +### POST /api/admin/extensions/{type}/{identifier}/recover + +- **라우트명**: `api.admin.extensions.recover` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ExtensionRecoveryController@recover` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.activate` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| type | path | string | 예 | module, plugin, template | 대상 확장의 타입 (module: 모듈, plugin: 플러그인, template: 템플릿). 타입에 맞는 Repository/Manager를 해석하는 데 사용되며 그 외 값은 422 | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.activate`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 코어와 재호환된 확장을 원클릭으로 복구(재활성화)합니다. 경로의 `{type}`/`{identifier}`로 대상을 지정하며, 대상이 `IncompatibleCore` 사유로 자동 비활성화된 상태인지 검증한 뒤 코어 버전 재검증을 거쳐 활성화합니다. 잘못된 타입은 422, 미존재 확장은 404, hidden 확장이나 자동 비활성화가 아닌 경우는 error_code와 함께 422를 반환하고, 재검증 실패 시 글로벌 핸들러가 core_version_mismatch로 변환합니다. `core.plugins.activate` 권한이 필요합니다. + + diff --git a/docs/backend/api/identity.md b/docs/backend/api/identity.md new file mode 100644 index 00000000..4eefee74 --- /dev/null +++ b/docs/backend/api/identity.md @@ -0,0 +1,1013 @@ +# Identity API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Identity 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/identity/logs + +- **라우트명**: `api.admin.identity.logs.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityLogController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.logs.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| provider_id | query | string | 아니오 | max 64 | provider 식별자 | +| purpose | query | string | 아니오 | max 64 | 인증 목적 필터 (signup/password_reset/self_update/sensitive_action 또는 모듈 정의 목적) | +| status | query | string | 아니오 | — | 상태 필터 (해당 상태의 항목만 조회) | +| channel | query | string | 아니오 | max 16 | 전송 채널 필터 (email 등 — 해당 채널로 시도한 이력만) | +| origin_type | query | string | 아니오 | — | 인증 트리거 출처 유형 필터 (route/hook/policy/middleware/api/custom/system — IdentityOriginType) | +| source_type | query | string | 아니오 | — | 정책 출처 필터 (core/module/plugin/admin — 어느 확장이 인증을 요구했는지, IdentityPolicySourceType) | +| source_identifier | query | string | 아니오 | max 100 | 출처 식별자 | +| provider_ids | query | array | 아니오 | — | provider 식별자 배열 | +| purposes | query | array | 아니오 | — | 인증 목적 다중선택 필터 (여러 목적 중 하나라도 일치) | +| statuses | query | array | 아니오 | — | 상태 다중선택 필터 (여러 상태 중 하나라도 일치) | +| channels | query | array | 아니오 | — | 전송 채널 다중선택 필터 (여러 채널 중 하나라도 일치) | +| origin_types | query | array | 아니오 | — | 출처 유형 다중선택 필터 (여러 origin_type 중 하나라도 일치) | +| user_id | query | integer | 아니오 | min 1 | user 식별자 | +| target_hash | query | string | 아니오 | — | 인증 대상 해시 필터 (SHA256(email\|phone), PII 원본 대신 해시로 추적) | +| search | query | string | 아니오 | max 64 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| search_type | query | string | 아니오 | `auto`, `user_id`, `target_hash`, `ip_address`, `policy_key` | 검색 유형 (검색 대상/방식 구분) | +| sort_by | query | string | 아니오 | `created_at`, `attempts` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| date_from | query | date | 아니오 | — | 조회 기간 시작일 | +| date_to | query | date | 아니오 | — | 조회 기간 종료일 | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | string | `e6ab6cd6-cdff-46cd-b4c2-b89dfda8745b` | 기본 키 (내부 식별자) | +| provider_id | string | `inicis` | provider 식별자 (연관 리소스 참조) | +| purpose | string | `sensitive_action` | 인증 목적 (signup/password_reset/self_update/sensitive_action 또는 모듈 정의 목적) | +| channel | string | `ipin` | 인증에 사용된 전송 채널 (email 등 코어 채널 또는 모듈 provider 자체 식별자) | +| user_id | integer | `130` | user 식별자 (연관 리소스 참조) | +| target_hash | string | `d88d36166bbeffc41eb6994e390f4715280c1…` | 인증 대상 해시 (SHA256(email\|phone) — PII 원본 저장 회피) | +| status | string | `cancelled` | 인증 시도 결과 상태 (requested/sent/processing/verified/failed/expired/cancelled/policy_violation_logged) | +| attempts | integer | `0` | 현재까지 누적된 검증 시도 횟수 | +| max_attempts | integer | `0` | 허용되는 최대 검증 시도 횟수 (초과 시 실패 처리) | +| ip_address | string | `127.0.0.1` | 요청/행위가 발생한 IP 주소 | +| user_agent | string | `Mozilla/5.0 (Windows NT 10.0; Win64; …` | 요청 클라이언트의 User-Agent 문자열 | +| origin_type | string | `api` | 인증 트리거 출처 유형 (route/hook/policy/middleware/api/custom/system — IdentityOriginType) | +| origin_identifier | string | `/api/identity/challenges` | 실제 트리거 경로/훅명 (예: PUT /api/me/password, core.user.before_update) | +| origin_policy_key | null | `null` | 정책이 인증을 강제한 경우 해당 identity_policies.key (정책 외 트리거는 null) | +| properties | null | `null` | 요청 페이로드 요약 (감사용 부가 정보, 없으면 null) | +| metadata | object | `{"mid":"INIiasTest","reqSvcCd":"03","mtxid_hash":"981546e…` | 프로바이더 내부 데이터 (코드 해시·외부 인증 식별자 등, PII 원본 미포함) | +| created_at | string | `2026-06-27 19:06:17` | 생성 일시 | +| verified_at | string | `2026-06-27 18:58:30` | verified 일시 | +| expires_at | string | `2026-06-27 19:21:17` | expires 일시 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 본인인증 시도 이력(성공/실패/취소/정책위반)을 관리자 화면에서 필터·검색·정렬하여 페이지네이션 조회합니다. `auth:sanctum` + `core.admin.identity.logs.read` 관리자 권한이 필요합니다. `IdentityLogService::search` 로 프로바이더·목적·상태·채널·기간 등 다중 필터를 적용하며, 응답의 `abilities.can_purge` 로 파기 권한 보유 여부를 함께 내려 UI 버튼 노출을 제어합니다. 관리자 IDV 이력 대시보드에서 특정 사용자(`user_id`)나 대상 해시(`target_hash`)로 감사 추적할 때 사용합니다. + + +### POST /api/admin/identity/logs/purge + +- **라우트명**: `api.admin.identity.logs.purge` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityLogController@purge` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.logs.purge` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| older_than_days | body | integer | 아니오 | min 1, max 3650 | 파기 기준 보관일수 (지정 일수보다 오래된 이력만 삭제, 미지정 시 기본 180일) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.purge`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 지정한 보관주기(`older_than_days`, 기본 180일) 를 경과한 본인인증 이력을 일괄 삭제하고 삭제된 행 수를 반환합니다. `auth:sanctum` + `core.admin.identity.logs.purge` 관리자 권한이 필요합니다. `IdentityLogService::purge` 가 실제 삭제를 수행하며 되돌릴 수 없으므로, 개인정보 보관기간 정책 준수를 위해 오래된 인증 시도 로그를 정리할 때 사용합니다. + + +### GET /api/admin/identity/messages/definitions + +- **라우트명**: `api.admin.identity.messages.definitions.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityMessageDefinitionController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.messages.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| provider_id | query | string | 아니오 | max 64 | provider 식별자 | +| scope_type | query | string | 아니오 | — | 메시지 정의 스코프 필터 (provider_default/purpose/policy — 어느 계층 템플릿인지, IdentityMessageScopeType) | +| scope_value | query | string | 아니오 | max 120 | 스코프 값 필터 (provider_default 빈값 / purpose 키 / policy 키) | +| extension_type | query | string | 아니오 | — | 확장 유형 (core/module/plugin/template) | +| extension_identifier | query | string | 아니오 | max 100 | 확장 식별자 | +| channel | query | string | 아니오 | max 20 | 메시지 채널 필터 (mail 등 — 해당 채널 템플릿을 가진 정의만) | +| is_active | query | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| sort_by | query | string | 아니오 | — | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity.message_definition.filter_index_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `1` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `1` | 기본 키 (내부 식별자) | +| provider_id | string | `g7:core.mail` | provider 식별자 (연관 리소스 참조) | +| scope_type | string | `provider_default` | 메시지 정의 스코프 (provider_default: 프로바이더 기본 / purpose: 목적별 / policy: 정책별 — IdentityMessageScopeType) | +| scope_value | string | `` | 스코프 값 (provider_default 빈 문자열 / purpose 목적 키 / policy 정책 키) | +| name | object | `{"ko":"메일 본인 확인 (기본)","en":"Mail Verification (default)",…` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| description | object | `{"ko":"특정 목적이 매칭되지 않을 때 사용되는 기본 메일 템플릿","en":"Fallback ma…` | 설명 (다국어 필드는 로케일별 값 객체) | +| channels | array | `["mail"]` | 이 정의가 지원하는 활성 채널 목록 (현재 mail, 향후 sms 등 확장) | +| variables | array | `[{"key":"code","description":"인증 코드 (text_code 흐름)"},{"ke…` | 템플릿에서 치환 가능한 변수 메타데이터 목록 (원소 key/description) | +| extension_type | string | `core` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) | +| extension_identifier | string | `core` | 이 리소스를 소유한 확장의 식별자 | +| is_active | boolean | `true` | active 여부 | +| is_default | boolean | `true` | default 여부 | +| user_overrides | array | `["name.ja"]` | 운영자가 시드 기본값에서 수정한 필드 경로 목록 (예: name.ja — 시더 재실행 시 보존 대상) | +| templates | array | `[{"id":1,"definition_id":1,"channel":"mail","subject":{"k…` | 이 정의에 속한 채널별 하위 메시지 템플릿 목록 (원소 id/channel/subject/body 등, eager load 시에만 포함) | +| created_at | string | `2026-05-27 15:20:18` | 생성 일시 | +| updated_at | string | `2026-06-30 13:33:16` | 최종 수정 일시 | +| abilities | object | `{"can_update":true,"can_delete":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 본인인증 알림 메시지 정의(프로바이더별·목적별·정책별 메일/SMS 템플릿 묶음) 목록을 필터·검색하여 페이지네이션 조회합니다. `auth:sanctum` + `core.admin.identity.messages.read` 관리자 권한이 필요합니다. 확장이 `core.identity.message_definition.filter_index_rules` 필터 훅으로 추가 검색 파라미터를 등록할 수 있습니다. 응답 각 항목의 `user_overrides` 로 운영자가 시드 기본값에서 수정한 필드를, `templates` 로 채널별 하위 템플릿을 함께 내려 관리자 메시지 설정 화면을 구성할 때 사용합니다. + + +### POST /api/admin/identity/messages/definitions + +- **라우트명**: `api.admin.identity.messages.definitions.store` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityMessageDefinitionController@store` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.messages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| provider_id | body | string | 예 | max 64 | provider 식별자 | +| scope_type | body | string | 예 | — | 메시지 정의 스코프 (관리자 생성은 policy 만 허용 — provider_default/purpose 는 시드 영역) | +| scope_value | body | string | 예 | max 120 | 스코프 값 (source_type='admin' 인 IdentityPolicy.key 와 일치해야 함) | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| description | body | array | 아니오 | — | 설명 | +| channels | body | array | 예 | min 1 | 지원 채널 목록 (최소 1개, 현재 mail 만 허용) | +| variables | body | array | 아니오 | — | 템플릿 치환 변수 메타데이터 목록 (원소 key/description, key 는 영문 식별자) | +| templates | body | array | 예 | min 1 | 채널별 하위 템플릿 배열 (최소 1개, 원소 channel/subject/body 다국어 배열) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity.message_definition.filter_store_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 운영자가 정책(policy) 매핑용 메시지 정의를 신규 생성합니다. `auth:sanctum` + `core.admin.identity.messages.update` 관리자 권한이 필요합니다. `IdentityMessageDefinitionService::createAdminDefinition` 이 처리하며 `scope_type='policy'` + `scope_value` 가 admin policy.key 와 매칭되는 경우만 허용됩니다(FormRequest 검증). `channels`·`templates` 를 최소 1개 이상 포함해야 하며, 확장은 `core.identity.message_definition.filter_store_rules` 필터 훅으로 검증 규칙을 확장할 수 있습니다. 특정 인증 정책에 전용 메일/SMS 문구를 붙이고자 할 때 사용하며 성공 시 201 로 응답합니다. + + +### DELETE /api/admin/identity/messages/definitions/{definition} + +- **라우트명**: `api.admin.identity.messages.definitions.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityMessageDefinitionController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.messages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| definition | path | string | 예 | — | 대상 definition의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 운영자가 추가한 메시지 정의를 삭제합니다. `auth:sanctum` + `core.admin.identity.messages.update` 관리자 권한이 필요합니다. 시드로 제공되는 `is_default=true` 정의는 삭제가 거부되어 403 을 반환하며(선언형 보호), 삭제 시 FK cascade 로 자식 템플릿이 함께 제거됩니다. 잘못 만들었거나 더 이상 쓰지 않는 정책 전용 메시지 정의를 정리할 때 사용합니다. + + +### GET /api/admin/identity/messages/definitions/{definition} + +- **라우트명**: `api.admin.identity.messages.definitions.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityMessageDefinitionController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.messages.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| definition | path | string | 예 | — | 대상 definition의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| provider_id | string | `g7:core.mail` | IDV 프로바이더 ID (예: g7:core.mail, kcp, portone) | +| scope_type | string | `provider_default` | 메시지 정의 스코프 (provider_default\|purpose\|policy) — App\Enums\IdentityMessageScopeType enum | +| scope_value | string | `` | 범위 값: provider_default 빈 문자열 / purpose 키 / policy 키 | +| name | object | `{"ko":"메일 본인 확인 (기본)","en":"Mail Verification (default)",…` | 다국어 표시명 ({"ko":"...", "en":"..."}) | +| description | object | `{"ko":"특정 목적이 매칭되지 않을 때 사용되는 기본 메일 템플릿","en":"Fallback ma…` | 다국어 설명 | +| channels | array | `["mail"]` | 활성 채널 (현재 ["mail"], 향후 sms 등 확장) | +| variables | array | `[{"key":"code","description":"인증 코드 (text_code 흐름)"},{"ke…` | 사용 가능 변수 메타데이터 ([{key, description}]) | +| extension_type | string | `core` | 확장 타입: core, module, plugin | +| extension_identifier | string | `core` | 확장 식별자 | +| is_active | boolean | `true` | active 여부 | +| is_default | boolean | `true` | default 여부 | +| user_overrides | array | `["name.ja"]` | 운영자가 수정한 필드명 목록 (예: ["name","is_active"]) | +| templates | array | `[{"id":1,"definition_id":1,"channel":"mail","subject":{"k…` | 이 정의에 속한 채널별 하위 메시지 템플릿 목록 (원소 id/channel/subject/body 등) | +| created_at | string | `2026-05-27 15:20:18` | 생성 일시 | +| updated_at | string | `2026-06-30 13:33:16` | 최종 수정 일시 | +| abilities | object | `{"can_update":true,"can_delete":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 단일 메시지 정의의 상세 정보를 조회합니다(하위 채널 템플릿 eager load 포함). `auth:sanctum` + `core.admin.identity.messages.read` 관리자 권한이 필요합니다. 관리자 편집 모달을 열 때 해당 정의의 다국어 이름/설명·활성 채널·사용 가능 변수·`user_overrides`·`templates` 전체를 로드하기 위해 사용합니다. + + +### PATCH /api/admin/identity/messages/definitions/{definition} + +- **라우트명**: `api.admin.identity.messages.definitions.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityMessageDefinitionController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.messages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| definition | path | string | 예 | — | 대상 definition의 식별자 | +| name | body | array | 아니오 | — | 대상의 이름/명칭 | +| description | body | array | 아니오 | — | 설명 | +| channels | body | array | 아니오 | min 1 | 지원 채널 목록 (최소 1개 — 이 정의가 발송할 채널 조정) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity.message_definition.filter_update_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 메시지 정의의 편집 가능 속성(`name`, `description`, `channels`, `is_active`) 을 수정합니다. `auth:sanctum` + `core.admin.identity.messages.update` 관리자 권한이 필요합니다. `IdentityMessageDefinitionService::updateDefinition` 이 처리하며 수정된 필드는 `user_overrides` 에 기록되어 시더 재실행 시에도 보존됩니다. 확장은 `core.identity.message_definition.filter_update_rules` 필터 훅으로 검증 규칙을 확장할 수 있습니다. 시드 정의의 표시명이나 활성 채널을 운영 상황에 맞게 조정할 때 사용합니다. + + +### POST /api/admin/identity/messages/definitions/{definition}/reset + +- **라우트명**: `api.admin.identity.messages.definitions.reset` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityMessageDefinitionController@reset` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.messages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| definition | path | string | 예 | — | 대상 definition의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 메시지 정의에 속한 모든 채널 템플릿을 시더 기본값으로 일괄 복원하고 정의를 default 상태로 되돌립니다. `auth:sanctum` + `core.admin.identity.messages.update` 관리자 권한이 필요합니다. 각 하위 템플릿에 대해 `IdentityMessageTemplateService::resetToDefault` 를 호출한 뒤 `markAsDefault` 로 정의를 표시하므로, 운영자가 수정한 문구를 한 번에 원상복구할 때 사용합니다. + + +### PATCH /api/admin/identity/messages/definitions/{definition}/toggle-active + +- **라우트명**: `api.admin.identity.messages.definitions.toggle-active` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityMessageDefinitionController@toggleActive` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.messages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| definition | path | string | 예 | — | 대상 definition의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 메시지 정의의 활성/비활성 상태를 토글합니다. `auth:sanctum` + `core.admin.identity.messages.update` 관리자 권한이 필요합니다. `IdentityMessageDefinitionService::toggleActive` 가 현재 `is_active` 값을 반전시키며, 특정 목적/정책용 인증 메시지 발송을 임시로 끄거나 다시 켤 때 사용합니다. + + +### POST /api/admin/identity/messages/templates/preview + +- **라우트명**: `api.admin.identity.messages.templates.preview` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityMessageTemplateController@preview` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.messages.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| template_id | body | integer | 예 | — | template 식별자 | +| data | body | array | 아니오 | — | 데이터 페이로드 | +| locale | body | string | 아니오 | max 10 | 로케일 코드 (표시 언어/지역) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 지정한 템플릿(`template_id`) 의 제목/본문에 변수(`data`) 를 치환한 결과를 지정 로케일(`locale`) 로 렌더링해 미리보기를 반환합니다. `auth:sanctum` + `core.admin.identity.messages.read` 관리자 권한이 필요합니다. `IdentityMessageTemplateService::getPreview` 가 실제 발송 없이 렌더 결과만 생성하므로, 운영자가 편집한 문구가 실제 메일/SMS 에서 어떻게 보일지 저장 전에 확인할 때 사용합니다. + + +### PATCH /api/admin/identity/messages/templates/{template} + +- **라우트명**: `api.admin.identity.messages.templates.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityMessageTemplateController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.messages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| template | path | string | 예 | — | 대상 template의 식별자 | +| subject | body | array | 아니오 | — | 제목 | +| body | body | array | 예 | — | 본문 | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity.message_template.filter_update_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 개별 채널 메시지 템플릿의 제목(`subject`)·본문(`body`)·활성 여부(`is_active`) 를 수정합니다. `auth:sanctum` + `core.admin.identity.messages.update` 관리자 권한이 필요합니다. `body` 는 필수이며 다국어 배열로 전달합니다. `IdentityMessageTemplateService::updateTemplate` 이 처리하고 확장은 `core.identity.message_template.filter_update_rules` 필터 훅으로 검증을 확장할 수 있습니다. 인증 메일/SMS 의 실제 발송 문구를 편집할 때 사용합니다. + + +### POST /api/admin/identity/messages/templates/{template}/reset + +- **라우트명**: `api.admin.identity.messages.templates.reset` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityMessageTemplateController@reset` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.messages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| template | path | string | 예 | — | 대상 template의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 개별 메시지 템플릿을 시더 기본값으로 복원합니다. `auth:sanctum` + `core.admin.identity.messages.update` 관리자 권한이 필요합니다. `IdentityMessageTemplateService::resetToDefault` 가 운영자 수정 내용을 폐기하고 최초 제공 문구로 되돌리므로, 특정 채널 템플릿 하나만 원상복구할 때 사용합니다(정의 전체 복원은 definition reset 사용). + + +### PATCH /api/admin/identity/messages/templates/{template}/toggle-active + +- **라우트명**: `api.admin.identity.messages.templates.toggle-active` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityMessageTemplateController@toggleActive` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.messages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| template | path | string | 예 | — | 대상 template의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 개별 메시지 템플릿의 활성/비활성 상태를 토글합니다. `auth:sanctum` + `core.admin.identity.messages.update` 관리자 권한이 필요합니다. `IdentityMessageTemplateService::toggleActive` 가 현재 `is_active` 값을 반전시키며, 특정 채널(예: 메일) 발송만 임시로 중단하거나 재개할 때 사용합니다. + + +### GET /api/admin/identity/policies + +- **라우트명**: `api.admin.identity.policies.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityPolicyController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.policies.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| scope | query | string | 아니오 | — | 조회 범위 한정 키 | +| purpose | query | string | 아니오 | max 64 | 인증 목적 필터 (해당 목적을 요구하는 정책만 조회) | +| source_type | query | string | 아니오 | — | 정책 출처 필터 (core/module/plugin/admin — 선언형 vs 운영자 정책 구분, IdentityPolicySourceType) | +| source_identifier | query | string | 아니오 | max 100 | 출처 식별자 | +| applies_to | query | string | 아니오 | — | 적용 대상 사용자 필터 (self: 일반 사용자 / admin: 관리자 / both: 모두, IdentityPolicyAppliesTo) | +| fail_mode | query | string | 아니오 | — | 실패 시 동작 필터 (block: 428 차단 / log_only: 감사 로그만, IdentityPolicyFailMode) | +| enabled | query | boolean | 아니오 | — | 사용 여부 | +| search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `25` | 기본 키 (내부 식별자) | +| key | string | `test` | 정책 식별자 (고유, 예: core.profile.password_change) | +| scope | string | `route` | 정책 적용 범위 (route: 라우트 패턴 / hook: Service 훅 / custom: 모듈 커스텀 키, IdentityPolicyScope) | +| target | string | `sdfsfsf` | 매칭 대상 (scope 에 따라 라우트명/URI 패턴, 훅 이름, 또는 custom key) | +| purpose | string | `inicis.adult_verification` | 이 정책이 요구하는 인증 목적 | +| provider_id | string | `inicis` | provider 식별자 (연관 리소스 참조) | +| grace_minutes | integer | `0` | 재인증 유예 시간(분) — 최근 N분 이내 동일 목적 인증 성공 시 재인증 생략 (0=매번 요구) | +| enabled | boolean | `true` | 정책 사용 여부 (false 시 인증 미강제) | +| priority | integer | `100` | 정책 우선순위 (같은 대상에 여러 정책 매칭 시 작을수록 우선) | +| conditions | array | `[]` | 추가 매칭 조건 (역할/HTTP 메서드/파라미터 매칭 조건 JSON, 없으면 빈 배열) | +| source_type | string | `admin` | 정책 출처 (core/module/plugin: 선언형 / admin: 운영자 직접 등록, IdentityPolicySourceType) | +| source_identifier | string | `sirsoft-board` | 출처 식별자 (선언형 정책의 소유 확장 identifier) | +| applies_to | string | `both` | 적용 대상 사용자 (self: 일반 사용자 / admin: 관리자 / both: 모두, IdentityPolicyAppliesTo) | +| fail_mode | string | `block` | 실패 시 동작 (block: HTTP 428 차단 / log_only: 감사 로그만 남기고 통과, IdentityPolicyFailMode) | +| user_overrides | array | `[]` | 운영자가 선언 기본값에서 재정의한 필드 목록 (선언형 정책만 의미, 시더 재실행 시 보존) | +| created_at | string | `2026-06-26 16:33:04` | 생성 일시 | +| updated_at | string | `2026-06-26 16:33:04` | 최종 수정 일시 | +| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.policies.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 본인인증 정책(어느 시점/위치에서 어떤 목적의 인증을 요구할지) 목록을 필터·검색하여 페이지네이션 조회합니다. `auth:sanctum` + `core.admin.identity.policies.read` 관리자 권한이 필요합니다. `IdentityPolicyService::search` 로 scope·purpose·source_type·enabled 등을 필터링하며, 응답의 `source_type` 으로 선언형(core/module/plugin) 정책과 운영자 정책(admin) 을 구분하고 `user_overrides` 로 운영자가 재정의한 필드를 표시합니다. 관리자 IDV 정책 DataGrid 를 구성할 때 사용합니다. + + +### POST /api/admin/identity/policies + +- **라우트명**: `api.admin.identity.policies.store` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityPolicyController@store` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.policies.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| key | body | string | 예 | max 120 | 정책 식별자 (고유, 예: core.profile.password_change) | +| scope | body | string | 예 | — | 조회 범위 한정 키 | +| target | body | string | 예 | max 255 | 매칭 대상 (scope 에 따라 라우트명/URI 패턴, 훅 이름, 또는 custom key) | +| purpose | body | string | 예 | max 64 | 이 정책이 요구하는 인증 목적 | +| provider_id | body | string | 아니오 | max 64 | provider 식별자 | +| grace_minutes | body | integer | 예 | min 0, max 43200 | 재인증 유예 시간(분) — 최근 N분 이내 동일 목적 인증 성공 시 재인증 생략 (0=매번 요구) | +| enabled | body | boolean | 아니오 | — | 사용 여부 | +| priority | body | integer | 아니오 | min 0, max 65535 | 우선순위 (작을수록 우선) | +| conditions | body | array | 아니오 | — | 추가 매칭 조건 (역할/HTTP 메서드/파라미터 매칭 조건 JSON) | +| applies_to | body | string | 예 | — | 적용 대상 사용자 (self: 일반 사용자 / admin: 관리자 / both: 모두, IdentityPolicyAppliesTo) | +| fail_mode | body | string | 예 | — | 실패 시 동작 (block: HTTP 428 차단 / log_only: 감사 로그만 남기고 통과, IdentityPolicyFailMode) | +| source_identifier | body | string | 아니오 | max 100 | 출처 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity_policy.store_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.policies.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 운영자가 새 본인인증 정책을 생성합니다(`source_type='admin'` 고정). `auth:sanctum` + `core.admin.identity.policies.update` 관리자 권한이 필요합니다. `IdentityPolicyService::createAdminPolicy` 가 처리하며 `key`·`scope`·`target`·`purpose`·`grace_minutes`·`applies_to`·`fail_mode` 등을 지정합니다. 확장은 `core.identity_policy.store_validation_rules` 필터 훅으로 검증을 확장할 수 있습니다. 특정 라우트/훅 지점에 코어가 선언하지 않은 인증 요구를 관리자가 직접 추가할 때 사용하며 성공 시 201 로 응답합니다. + + +### DELETE /api/admin/identity/policies/{id} + +- **라우트명**: `api.admin.identity.policies.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityPolicyController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.policies.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity_policy.destroy_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.policies.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 본인인증 정책을 삭제합니다. `auth:sanctum` + `core.admin.identity.policies.update` 관리자 권한이 필요합니다. `source_type='admin'` 인 운영자 정책만 삭제할 수 있고, 선언형 정책(core/module/plugin) 은 403 을 반환하므로 삭제 대신 비활성화(update 로 `enabled=false`) 로 대체해야 합니다. 확장은 `core.identity_policy.destroy_validation_rules` 필터 훅으로 검증을 확장할 수 있습니다. 운영자가 잘못 만든 정책을 제거할 때 사용합니다. + + +### PUT /api/admin/identity/policies/{id} + +- **라우트명**: `api.admin.identity.policies.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityPolicyController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.policies.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| enabled | body | boolean | 아니오 | — | 사용 여부 | +| grace_minutes | body | integer | 아니오 | min 0, max 43200 | 재인증 유예 시간(분) — 최근 N분 이내 동일 목적 인증 성공 시 재인증 생략 (0=매번 요구) | +| provider_id | body | string | 아니오 | max 64 | provider 식별자 | +| fail_mode | body | string | 아니오 | — | 실패 시 동작 (block: HTTP 428 차단 / log_only: 감사 로그만 남기고 통과, IdentityPolicyFailMode) | +| key | body | string | 아니오 | max 120 | 정책 식별자 (admin 정책만 변경 가능, 선언형 정책은 확장 지점 식별자라 변경 차단) | +| scope | body | string | 아니오 | — | 조회 범위 한정 키 | +| target | body | string | 아니오 | max 255 | 매칭 대상 (admin 정책만 변경 가능, 선언형 정책은 변경 차단) | +| purpose | body | string | 아니오 | max 64 | 이 정책이 요구하는 인증 목적 | +| priority | body | integer | 아니오 | min 0, max 65535 | 우선순위 (작을수록 우선) | +| conditions | body | array | 아니오 | — | 추가 매칭 조건 (역할/HTTP 메서드/파라미터 매칭 조건 JSON) | +| applies_to | body | string | 아니오 | — | 적용 대상 사용자 (self: 일반 사용자 / admin: 관리자 / both: 모두, IdentityPolicyAppliesTo) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity_policy.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.policies.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 본인인증 정책을 수정합니다. `auth:sanctum` + `core.admin.identity.policies.update` 관리자 권한이 필요합니다. `source_type='admin'` 정책은 모든 필드를 편집할 수 있으나, 선언형 정책(core/module/plugin) 은 `enabled`·`grace_minutes`·`provider_id`·`fail_mode`·`conditions`·`purpose`·`applies_to`·`priority` 화이트리스트("어떻게 인증할지") 만 허용되고 `key`/`scope`/`target`("어디서 인증할지") 은 확장 지점 식별자라 변경이 차단됩니다. 편집한 필드는 `user_overrides` 에 append 되어 시더 재실행 시 보존됩니다. 허용 필드가 하나도 없으면 422(nothing_to_update) 를 반환합니다. 확장은 `core.identity_policy.update_validation_rules` 필터 훅으로 검증을 확장할 수 있습니다. + + +### POST /api/admin/identity/policies/{id}/reset-field + +- **라우트명**: `api.admin.identity.policies.reset-field` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityPolicyController@resetField` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.policies.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| field | body | string | 예 | `enabled`, `grace_minutes`, `provider_id`, `fail_mode`, `conditions`, `purpose`, `applies_to`, `priority` | 기본값으로 되돌릴 대상 필드명 (해당 필드의 운영자 재정의 해제 후 선언 기본값 복원) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity_policy.reset_field_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.policies.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 선언형 정책의 특정 필드에 대한 운영자 재정의(`user_overrides`) 를 해제하고 선언 기본값으로 즉시 복원합니다. `auth:sanctum` + `core.admin.identity.policies.update` 관리자 권한이 필요합니다. `field` 는 재정의 가능 필드(`enabled`, `grace_minutes`, `provider_id`, `fail_mode`, `conditions`, `purpose`, `applies_to`, `priority`) 중 하나여야 합니다. `source_type='admin'` 정책은 선언 기본값이 없어 403 을 반환하며 선언형 정책(core/module/plugin) 에만 의미가 있습니다. 관리자 편집 화면의 "↺ 기본값으로 되돌리기" 버튼이 호출하는 엔드포인트입니다. + + +### GET /api/admin/identity/providers + +- **라우트명**: `api.admin.identity.providers.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\Identity\AdminIdentityProviderController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.admin.identity.providers.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | string | `g7:core.mail` | 기본 키 (내부 식별자) | +| label | string | `이메일` | 표시용 라벨 | +| channels | array | `["email"]` | 이 프로바이더가 지원하는 전송 채널 식별자 목록 | +| channel_labels | object | `{"email":"이메일"}` | 채널 식별자 → 사람이 읽는 표시 라벨 맵 (다국어 처리, UI 표시용) | +| render_hint | string | `text_code` | 프론트 challenge 렌더 방식 힌트 (text_code: 코드 입력 UI / link: 링크 클릭 유도 / external_redirect: 외부 인증 페이지 이동) | +| is_available | boolean | `true` | available 여부 | +| abilities | object | `{"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +| settings_schema | object | `{"code_length":{"label":"인증 코드 길이","type":"integer","defa…` | 관리자 설정 UI 반복 렌더용 설정 스키마 (필드별 label/type/default/options/help — 코드 길이·만료 시간 등) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.providers.read`)이 없는 경우 | + + + +**설명** 등록된 IDV 프로바이더 목록을 각 프로바이더의 설정 스키마(`settings_schema`) 와 함께 반환합니다. `auth:sanctum` + `core.admin.identity.providers.read` 관리자 권한이 필요합니다. 각 프로바이더의 `getSettingsSchema()` 결과를 `core.identity.settings_schema` 필터 훅으로 확장 가능하게 통과시키므로, 관리자 프로바이더 설정 카드(코드 길이·만료 시간 등)를 스키마 기반으로 반복 렌더링할 때 사용합니다. 설정 스키마가 없는 공개용 목록은 `GET /api/identity/providers` 를 사용합니다. + + +### POST /api/identity/callback/{providerId} + +- **라우트명**: `api.identity.callback` +- **컨트롤러**: `\App\Http\Controllers\Api\Identity\IdentityVerificationController@callback` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| providerId | path | string | 예 | — | 대상 provider의 식별자 | +| challenge_id | body | string | 예 | max 64 | challenge 식별자 | +| code | body | string | 아니오 | max 512 | 외부 프로바이더가 콜백으로 전달한 인가 코드/인증 코드 | +| token | body | string | 아니오 | max 1024 | 인증/검증 토큰 | +| state | body | string | 아니오 | max 512 | 콜백 위변조 방지용 state 값 (요청 시 발급한 값과 대조) | +| redirect_url | body | string | 아니오 | max 2048 | 인증 완료 후 되돌아갈 URL (open redirect 방지 위해 same-origin 만 허용) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 외부 IDV 프로바이더(외부 인증 SDK/OAuth-style provider) 가 사용자 브라우저를 앱으로 되돌려 보내는 redirect 콜백 진입점입니다. `auth:sanctum` 인증이 필요합니다. body/query 의 `challenge_id` 를 추출해 `IdentityVerificationService::handleProviderCallback` 에 위임하며, 클라이언트가 stash 한 `return` 쿼리 유무와 성공 여부에 따라 응답이 갈립니다 — 성공+return: 302 로 `{return}?verification_token=...&challenge_id=...`, 성공+return 없음: 200 JSON `{ verification_token }`, 실패+return: 302 로 `{return}?identity_error={failure_code}`, 실패+return 없음: 422 JSON. `return` URL 은 open redirect 방지를 위해 same-origin(또는 `/` 상대경로) 만 허용하고 protocol-relative(`//`) 는 차단합니다. + + +### POST /api/identity/challenges + +- **라우트명**: `api.identity.challenges.request` +- **컨트롤러**: `\App\Http\Controllers\Api\Identity\IdentityVerificationController@request` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.identity.request` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| purpose | body | string | 예 | max 64 | 인증 목적 (signup/password_reset/self_update/sensitive_action 또는 모듈 정의 목적) | +| target | body | array | 아니오 | — | 비로그인 게스트의 인증 대상 (target.email 또는 target.phone — 로그인 사용자는 본인으로 자동 설정) | +| provider_id | body | string | 아니오 | max 64 | provider 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity.request_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.identity.request`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 본인인증 challenge(인증 시도) 를 시작합니다. `auth:sanctum` + `core.identity.request` 권한이 필요합니다. 로그인 사용자는 인증 대상이 자동으로 본인이 되며, 비로그인 게스트(Mode B 가입 흐름) 는 `target.email` 또는 `target.phone` 을 반드시 제공해야 하고 없으면 422(missing_target) 를 반환합니다. `provider_id` 미지정 시 목적에 매핑된 기본 프로바이더가 선택됩니다. `IdentityVerificationService::start` 가 IP·User-Agent 등 컨텍스트와 함께 challenge 를 생성하고 성공 시 201 로 challenge 리소스(render_hint 등 포함) 를 반환합니다. 확장은 `core.identity.request_validation_rules` 필터 훅으로 파라미터를 추가할 수 있습니다. + + +### GET /api/identity/challenges/{challenge} + +- **라우트명**: `api.identity.challenges.show` +- **컨트롤러**: `\App\Http\Controllers\Api\Identity\IdentityVerificationController@show` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| challenge | path | string | 예 | — | 대상 challenge의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | string | `00484973-8cd3-4a1d-85f2-78361feb6f0d` | 기본 키 (내부 식별자) | +| status | string | `verified` | requested\|sent\|processing\|verified\|failed\|expired\|cancelled\|policy_violation_logged | +| provider_id | string | `inicis` | 프로바이더 식별자 (예: g7:core.mail, kcp) | +| purpose | string | `sensitive_action` | 인증 목적 (signup\|password_reset\|self_update\|sensitive_action\|*module-defined*) — 코어 4종은 App\Enums\IdentityVerificationPurpose enum, 모듈/플러그인은 declaredPurposes 레지스트리 | +| render_hint | string | `text_code` | 프론트 렌더 힌트 (text_code\|link\|external_redirect) | +| expires_at | string | `2026-05-12T18:14:19+00:00` | expires 일시 | +| attempts | integer | `3` | 시도 횟수 | +| max_attempts | integer | `5` | 허용 최대 시도 횟수 | +| public_payload | array | `[]` | 프론트 렌더에 필요한 공개 안전 페이로드 (민감 metadata 제외, 프로바이더별 UI 힌트 — 없으면 빈 배열) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** challenge 의 공개 상태를 폴링 조회합니다(engine-v1.46.0+). `auth:sanctum` 인증이 필요합니다. Stripe Identity/토스인증 push/외부 redirect 콜백 대기처럼 verify 즉시 응답을 받지 못하는 비동기 검증 흐름에서 클라이언트가 상태(verified/failed/expired 등) 를 추적하기 위한 엔드포인트입니다. `IdentityVerificationService::getStatus` 가 공개 안전 항목만 노출하며(시도 횟수 상세·코드 본체·metadata 미노출), challenge 를 찾지 못하면 404 를 반환합니다. + + +### POST /api/identity/challenges/{challenge}/cancel + +- **라우트명**: `api.identity.challenges.cancel` +- **컨트롤러**: `\App\Http\Controllers\Api\Identity\IdentityVerificationController@cancel` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.identity.cancel` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| challenge | path | string | 예 | — | 대상 challenge의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.identity.cancel`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 진행 중인 challenge 를 취소합니다. `auth:sanctum` + `core.identity.cancel` 권한이 필요합니다. 라우트 모델 바인딩 + PermissionMiddleware 의 scope=self 가드로 로그인 사용자는 본인 challenge 만 취소할 수 있으며, 비로그인 게스트는 guest 역할 권한으로 진입합니다(모달 취소 시 audit trail 정합용). `IdentityVerificationService::cancel` 이 처리하고 대상 challenge 가 없으면 404 를 반환합니다. 사용자가 인증 모달을 닫을 때 서버 상태를 cancelled 로 남겨 이력 정합성을 맞추는 데 사용합니다. + + +### POST /api/identity/challenges/{challenge}/verify + +- **라우트명**: `api.identity.challenges.verify` +- **컨트롤러**: `\App\Http\Controllers\Api\Identity\IdentityVerificationController@verify` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.identity.verify` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| challenge | path | string | 예 | — | 대상 challenge의 식별자 | +| code | body | string | 아니오 | max 16 | 사용자가 입력한 인증 코드 (text_code 흐름 — 메일/SMS 로 받은 숫자 코드) | +| token | body | string | 아니오 | max 256 | 인증/검증 토큰 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.identity.verify_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.identity.verify`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** challenge 를 검증(인증 완료) 합니다. `auth:sanctum` + `core.identity.verify` 권한이 필요합니다. 라우트 모델 바인딩 + PermissionMiddleware 의 scope=self 가드로 로그인 사용자는 본인 challenge 만 검증하며, 비로그인 게스트는 guest 역할 권한으로 진입합니다(Mode B 가입 흐름). `code`(text_code 흐름) 또는 `token`(link/redirect 흐름) 을 전달하고 `IdentityVerificationService::verify` 가 처리합니다. 실패 시 422 로 `failure_code` 와 서버 기준 `attempts`/`max_attempts` 를 함께 내려 클라이언트의 "남은 시도 횟수" UI 를 서버와 동기화하며, 성공 시 후속 민감 작업에 제출할 `verification_token` 을 반환합니다. 확장은 `core.identity.verify_validation_rules` 필터 훅으로 파라미터를 추가할 수 있습니다. + + +### GET /api/identity/policies/resolve + +- **라우트명**: `api.identity.policies.resolve` +- **컨트롤러**: `App\Http\Controllers\Api\Identity\IdentityVerificationController@resolvePolicy` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| scope | query | string | 예 | max 32 | 조회 범위 한정 키 | +| target | query | string | 예 | max 255 | 정책 매칭 대상 (scope 와 함께 해석 — 라우트명/URI 패턴, 훅 이름, 또는 custom key) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 지정한 `scope`+`target` 조합에 매칭되는 본인인증 정책 요약을 반환합니다(프론트엔드 프리페치용). `auth:sanctum` 인증이 필요합니다. `IdentityPolicyService::resolve` 로 정책을 찾아 활성(`enabled`) 정책이 없으면 `data: null` 을, 있으면 UI 힌트에 필요한 최소 필드(`policy_key`, `scope`, `target`, `purpose`, `provider_id`, `grace_minutes`, `applies_to`, `fail_mode`) 만 반환하고 민감 필드는 노출하지 않습니다. 레이아웃 마운트 시 "이 페이지에서 IDV 가 요구될 수 있는 API" 를 미리 파악해 버튼 배지("확인 필요") 같은 UI 힌트를 표시할 때 사용합니다. + + +### GET /api/identity/providers + +- **라우트명**: `api.identity.providers.index` +- **컨트롤러**: `App\Http\Controllers\Api\Identity\IdentityVerificationController@providers` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | string | `g7:core.mail` | 기본 키 (내부 식별자) | +| label | string | `이메일` | 표시용 라벨 | +| channels | array | `["email"]` | 이 프로바이더가 지원하는 전송 채널 식별자 목록 | +| channel_labels | object | `{"email":"이메일"}` | 채널 식별자 → 사람이 읽는 표시 라벨 맵 (다국어 처리, UI 표시용) | +| render_hint | string | `text_code` | 프론트 challenge 렌더 방식 힌트 (text_code: 코드 입력 UI / link: 링크 클릭 유도 / external_redirect: 외부 인증 페이지 이동) | +| is_available | boolean | `true` | available 여부 | +| abilities | object | `{"can_update":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +_대표 에러 없음 (공개 조회). 인증·권한 미요구 엔드포인트로 도메인 특이 에러를 반환하지 않습니다._ + + + +**설명** 등록된 IDV 프로바이더의 공개 메타데이터 목록을 반환합니다. 공개 엔드포인트로 인증이 필요하지 않으며, 비로그인 가입 흐름에서도 접근합니다. `IdentityVerificationManager::all()` 의 각 프로바이더를 `ProviderResource` 로 직렬화해 id·label·channels·render_hint·is_available 등만 노출하고, 관리자용과 달리 `settings_schema` 는 포함하지 않습니다. 인증 모달이 사용 가능한 프로바이더 선택지를 표시할 때 사용합니다. + + +### GET /api/identity/purposes + +- **라우트명**: `api.identity.purposes.index` +- **컨트롤러**: `App\Http\Controllers\Api\Identity\IdentityVerificationController@purposes` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | string | `signup` | 기본 키 (내부 식별자) | +| label | string | `회원가입 인증` | 표시용 라벨 | +| description | string | `신규 가입자의 이메일/전화번호 소유 확인.` | 설명 (다국어 필드는 로케일별 값 객체) | +| default_provider | string | `g7:core.mail` | 이 목적에 매핑된 기본 프로바이더 ID (요청 시 provider_id 미지정이면 이 값 사용, 미설정 시 null) | +| allowed_channels | array | `["mail","sms"]` | 이 목적에서 사용 가능한 전송 채널 목록 | +| source_type | string | `core` | 목적 출처 (core: 코어 기본 4종 / module / plugin — 어느 확장이 선언했는지) | +| source_identifier | string | `core` | 출처 식별자 (목적을 선언한 확장의 identifier) | + +**에러 응답** + +_대표 에러 없음 (공개 조회). 인증·권한 미요구 엔드포인트로 도메인 특이 에러를 반환하지 않습니다._ + + + +**설명** 등록된 인증 목적(purpose) 목록을 반환합니다. 공개 엔드포인트로 인증이 필요하지 않습니다. `IdentityVerificationManager::getAllPurposes()` 로 코어 기본 4종(signup/password_reset/self_update/sensitive_action) + 활성 모듈/플러그인의 `getIdentityPurposes()` 선언 + `core.identity.purposes` 필터 훅(서드파티 동적 확장) 을 병합하며, 각 항목의 label/description 은 i18n 키·다국어 배열·평문 세 형태를 현재 로케일 문자열로 정규화해 내려줍니다. 각 목적의 기본 프로바이더·허용 채널·출처(source_type/source_identifier) 를 함께 반환하므로, 인증 UI 가 목적별 선택지와 문구를 구성할 때 사용합니다. + + diff --git a/docs/backend/api/language-packs.md b/docs/backend/api/language-packs.md new file mode 100644 index 00000000..2e4a98a9 --- /dev/null +++ b/docs/backend/api/language-packs.md @@ -0,0 +1,500 @@ +# Language Packs API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Language Packs 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/language-packs + +- **라우트명**: `api.admin.language-packs.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| scope | query | string | 아니오 | — | 조회 범위 한정 키 | +| target_identifier | query | string | 아니오 | max 150 | 대상 확장 식별자 | +| locale | query | string | 아니오 | max 20 | 로케일 코드 (표시 언어/지역) | +| status | query | string | 아니오 | — | 상태 필터 (해당 상태의 항목만 조회) | +| vendor | query | string | 아니오 | max 100 | 벤더명 (확장 제작자 식별자) | +| search | query | string | 아니오 | max 150 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| exclude_protected | query | boolean | 아니오 | — | 보호 항목 제외 여부 | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `16` | 기본 키 (내부 식별자) | +| identifier | string | `g7-core-en` | 언어팩 고유 식별자 ({vendor}-{scope}-{target?}-{locale}) | +| vendor | string | `g7` | 언어팩 제작자 식별자 | +| scope | string | `core` | 적용 대상 분류 | +| target_identifier | string | `gnuboard7-hello_module` | 대상 확장 식별자 (scope=core일 때 null) | +| locale | string | `en` | IETF BCP-47 locale 태그 | +| locale_name | string | `EN` | 영문 언어명 | +| locale_native_name | string | `English` | 원어 언어명 | +| text_direction | string | `ltr` | 텍스트 방향 | +| version | string | `7.0.1` | 언어팩 버전 | +| 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` | 언어팩 상태 | +| is_protected | boolean | `true` | protected 여부 | +| source_type | string | `built_in` | 설치 소스 유형 (zip/github/url/bundled/bundled_with_extension) | +| origin | string | `built_in` | 출처 (설치/등록 원천 구분 값) | +| source_url | string | `lang/en` | 설치 소스 URL 또는 경로 | +| github_url | null | `null` | GitHub 저장소 URL (manifest 파생) | +| github_changelog_url | null | `null` | GitHub 변경 이력(CHANGELOG) URL (manifest 파생) | +| bundled_identifier | string | `g7-module-gnuboard7-hello_module-ja` | 대응하는 번들 확장 식별자 (번들 원본 매칭용) | +| install_blocked_reason | string | `target_not_installed` | 설치가 차단된 사유 (차단 없으면 null) | +| target_name | string | `게시판` | 대상 확장의 표시 이름 (scope+target_identifier 로 해석) | +| installed_at | string | `2026-07-03 19:20:23` | installed 일시 | +| activated_at | string | `2026-07-03 19:20:23` | activated 일시 | +| created_at | string | `2026-07-06 19:20:23` | 생성 일시 | +| updated_at | string | `2026-07-06 19:20:23` | 최종 수정 일시 | +| has_update | boolean | `false` | update 여부 | +| abilities | object | `{"can_activate":true,"can_deactivate":true,"can_uninstall…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 설치된 언어팩과 번들 소스로부터 노출되는 언어팩 목록을 페이지네이션으로 조회합니다. `core.language_packs.read` 권한이 필요합니다. `scope`/`locale`/`status`/`vendor`/`search` 등으로 필터링하고 `exclude_protected` 로 보호(protected) 팩을 제외할 수 있습니다. 관리자 언어팩 관리 화면의 목록/필터 표시에 사용합니다. + + +### POST /api/admin/language-packs/bulk-activate + +- **라우트명**: `api.admin.language-packs.bulk-activate` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@bulkActivate` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.language_packs.bulk_activate_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.manage`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 전달된 `ids` 배열의 언어팩을 일괄 활성화합니다. `core.language_packs.manage` 권한이 필요합니다. 성공/실패를 분리한 결과를 반환하므로 일부만 실패해도 전체가 롤백되지 않습니다. 비활성 팩을 한 번에 재활성화하는 reactivate 모달의 "활성화" 동작에 사용합니다. + + +### POST /api/admin/language-packs/check-updates + +- **라우트명**: `api.admin.language-packs.check-updates` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@checkUpdates` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.update` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.update`)이 없는 경우 | + + + +**설명** GitHub 소스로 설치된 언어팩들의 원격 최신 버전을 조회해 업데이트 가능 여부를 확인합니다. `core.language_packs.update` 권한이 필요합니다. 실제 업데이트를 수행하지 않고 검사 결과(checked, updates, details)만 반환하며, 외부 GitHub 호출을 동반합니다. 언어팩 목록의 업데이트 배지 표시에 사용합니다. + + +### POST /api/admin/language-packs/install-from-bundled + +- **라우트명**: `api.admin.language-packs.install-from-bundled` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@installFromBundled` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | body | string | 예 | max 200 | 대상 확장/리소스의 식별자 | +| auto_activate | body | boolean | 아니오 | — | 설치 후 자동 활성화 여부 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** `lang-packs/_bundled/{identifier}` 디렉토리의 번들 소스에서 언어팩을 설치(또는 재설치)합니다. `core.language_packs.install` 권한이 필요합니다. `auto_activate` 가 true면 설치 후 곧바로 활성화합니다. 코어/확장에 선탑재된 번들 언어팩을 DB에 등록할 때 사용합니다. + + +### POST /api/admin/language-packs/install-from-file + +- **라우트명**: `api.admin.language-packs.install-from-file` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@installFromFile` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| file | body | file | 예 | max 10240 | 업로드 파일 | +| auto_activate | body | boolean | 아니오 | — | 설치 후 자동 활성화 여부 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 업로드된 ZIP 파일에서 언어팩을 설치합니다. `core.language_packs.install` 권한이 필요합니다. manifest 검증에 실패하면 422로 응답하며, `auto_activate` 가 true면 설치 후 즉시 활성화합니다. 관리자가 로컬 ZIP 파일을 직접 업로드해 언어팩을 추가하는 화면에 사용합니다. + + +### POST /api/admin/language-packs/install-from-github + +- **라우트명**: `api.admin.language-packs.install-from-github` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@installFromGithub` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| github_url | body | string | 예 | — | GitHub 저장소 URL | +| auto_activate | body | boolean | 아니오 | — | 설치 후 자동 활성화 여부 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** GitHub 저장소 URL에서 언어팩을 다운로드해 설치합니다. `core.language_packs.install` 권한이 필요합니다. 외부 GitHub 호출을 동반하며 manifest 검증 실패 시 422로 응답합니다. `auto_activate` 가 true면 설치 후 즉시 활성화합니다. GitHub로 배포되는 언어팩을 URL 만으로 설치할 때 사용하며, 이후 check-updates/update 로 갱신을 추적할 수 있습니다. + + +### POST /api/admin/language-packs/install-from-url + +- **라우트명**: `api.admin.language-packs.install-from-url` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@installFromUrl` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| url | body | string | 예 | max 500 | URL | +| checksum | body | string | 아니오 | — | 무결성 검증 체크섬 (SHA-256) | +| auto_activate | body | boolean | 아니오 | — | 설치 후 자동 활성화 여부 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 임의의 URL에서 언어팩 ZIP을 내려받아 설치합니다. `core.language_packs.install` 권한이 필요합니다. `checksum` 을 함께 전달하면 다운로드 무결성을 검증하며, manifest 검증 실패 시 422로 응답합니다. `auto_activate` 가 true면 설치 후 즉시 활성화합니다. GitHub 외 임의 호스팅에 배포된 언어팩을 설치할 때 사용합니다. + + +### POST /api/admin/language-packs/manifest-preview + +- **라우트명**: `api.admin.language-packs.manifest-preview` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@manifestPreview` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| file | body | file | 예 | max 5120 | 업로드 파일 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 업로드된 ZIP을 실제로 설치하지 않고 manifest 와 검증 결과만 미리 조회합니다. `core.language_packs.install` 권한이 필요합니다. 부수 효과 없이 읽기만 수행하며 검증 실패 시 422로 응답합니다. 설치 확인 모달에서 대상 언어팩의 메타데이터와 유효성을 미리 보여줄 때 사용합니다. + + +### POST /api/admin/language-packs/refresh-cache + +- **라우트명**: `api.admin.language-packs.refresh-cache` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@refreshCache` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.manage` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.manage`)이 없는 경우 | + + + +**설명** 번역/레지스트리/템플릿 언어 캐시를 무효화합니다. `core.language_packs.manage` 권한이 필요합니다. 언어팩 파일을 직접 수정했거나 활성 상태가 프론트에 반영되지 않을 때 캐시를 강제로 갱신하는 용도로 사용합니다. + + +### DELETE /api/admin/language-packs/{id} + +- **라우트명**: `api.admin.language-packs.uninstall` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@uninstall` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| cascade | query | boolean | 아니오 | — | 연쇄 처리 여부 (의존 항목 함께 처리) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.manage`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 언어팩을 제거하고 설치된 파일을 삭제합니다. `core.language_packs.manage` 권한이 필요합니다. `cascade` 가 true면 연관 자원까지 함께 제거합니다. 대상이 존재하지 않으면 404로 응답합니다. 관리자 언어팩 관리 화면의 삭제(제거) 동작에 사용합니다. + + +### GET /api/admin/language-packs/{id} + +- **라우트명**: `api.admin.language-packs.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 단일 언어팩의 상세 정보를 조회합니다. `core.language_packs.read` 권한이 필요합니다. `{id}` 가 정수면 DB 레코드를, 문자열(번들 식별자)이면 `lang-packs/_bundled/{id}` manifest 로 합성된 가상 행을 반환합니다. 미설치 번들 언어팩까지 상세 모달로 열람할 수 있도록 하며, 없으면 404로 응답합니다. + + +### POST /api/admin/language-packs/{id}/activate + +- **라우트명**: `api.admin.language-packs.activate` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@activate` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.manage`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 언어팩을 활성화합니다(슬롯 스위칭). `core.language_packs.manage` 권한이 필요합니다. 동일 슬롯(scope·target·locale)에 이미 다른 활성 팩이 있으면 409(slot_conflict)로 현재/대상 팩을 함께 반환하며, 프론트는 확인 모달을 띄운 뒤 `force=true` 로 재호출해 교체합니다. 대상이 없으면 404로 응답합니다. + + +### GET /api/admin/language-packs/{id}/changelog + +- **라우트명**: `api.admin.language-packs.changelog` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@changelog` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 언어팩의 CHANGELOG.md 내용을 반환합니다. `core.language_packs.read` 권한이 필요합니다. `{id}` 가 정수면 DB 레코드를, 문자열이면 번들 가상 행을 사용합니다. 파싱된 항목(entries), 원문(changelog), 존재 여부(has_changelog)를 함께 반환하며, CHANGELOG 파일이 없어도 빈 값으로 정상 응답합니다. 상세 모달의 변경 이력 탭에 사용합니다. + + +### POST /api/admin/language-packs/{id}/deactivate + +- **라우트명**: `api.admin.language-packs.deactivate` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@deactivate` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.manage`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 활성 언어팩을 비활성화합니다. `core.language_packs.manage` 권한이 필요합니다. 해당 슬롯의 번역 적용이 해제되며, 대상이 없으면 404로 응답합니다. 관리자 언어팩 관리 화면에서 활성 팩을 끄는 동작에 사용합니다. + + +### POST /api/admin/language-packs/{id}/update + +- **라우트명**: `api.admin.language-packs.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@performUpdate` +- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.language_packs.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** GitHub 소스 언어팩을 최신 버전으로 다시 내려받아 적용합니다. `core.language_packs.update` 권한이 필요합니다. 외부 GitHub 재다운로드와 파일 교체를 동반하며, 갱신된 언어팩 정보를 반환합니다. 대상이 없으면 404로 응답합니다. check-updates 로 업데이트가 감지된 팩을 실제로 갱신할 때 사용합니다. + + diff --git a/docs/backend/api/layouts.md b/docs/backend/api/layouts.md new file mode 100644 index 00000000..12457a03 --- /dev/null +++ b/docs/backend/api/layouts.md @@ -0,0 +1,74 @@ +# Layouts API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Layouts 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/layouts/preview/{token}.json + +- **라우트명**: `api.public.layouts.preview.serve` +- **컨트롤러**: `App\Http\Controllers\Api\Public\LayoutPreviewController@serve` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| token | path | string | 예 | — | 인증/검증 토큰 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 편집 중인 레이아웃을 미리보기 토큰(UUID)으로 조회해 JSON으로 서빙합니다. 토큰으로 대상 레이아웃을 찾은 뒤 상속 병합과 확장(extension) 적용까지 마친 결과를 반환하며, 토큰이 유효하지 않으면 404를 반환합니다. 인증 미들웨어가 적용되지만 실질적 보안 메커니즘은 토큰 자체이며, 레이아웃 편집기에서 저장 전 변경분을 실제 렌더링으로 확인하는 용도입니다. + + +### GET /api/layouts/{templateIdentifier}/{layoutName}.json + +- **라우트명**: `api.public.layouts.serve` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicLayoutController@serve` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateIdentifier | path | string | 예 | — | 대상 template의 식별자 | +| layoutName | path | string | 예 | — | 대상 layout의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 활성 템플릿의 병합된 레이아웃 JSON을 프론트엔드에 서빙합니다. 템플릿이 존재하고 활성 상태여야 하며, 상속 병합·확장 적용을 마친 결과를 ETag·Cache-Control 헤더와 함께 반환하고 미변경 시 304로 응답합니다. 레이아웃의 `permissions`에 따라 접근을 제한하고(비회원 401, 권한 부족 403), 컴포넌트 단위 권한 필터링을 사용자별로 적용합니다. 쿼리 `v`(정수 캐시 버전)로 캐시를 구분하며, `with_source_meta=1`은 `core.templates.layouts.edit` 권한이 있어야 노드별 출처 메타(편집기 전용)를 포함해 반환합니다. + + diff --git a/docs/backend/api/license.md b/docs/backend/api/license.md new file mode 100644 index 00000000..6647eb90 --- /dev/null +++ b/docs/backend/api/license.md @@ -0,0 +1,48 @@ +# License API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 License 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/license + +- **라우트명**: `api.admin.license` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LicenseController@core` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| content | string | `프로그램 명칭 : 그누보드7 (Gnuboard7) 저작자 : (주…` | 본문 내용 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 코어 라이선스 파일의 원문 텍스트를 `content`로 반환합니다. `auth:sanctum` 인증이 필요하며, 라이선스 파일이 없으면 404(`common.not_found`)를 반환합니다. 관리자 화면에서 코어 저작권·라이선스 고지를 표시하는 용도로 사용합니다. + + diff --git a/docs/backend/api/locales.md b/docs/backend/api/locales.md new file mode 100644 index 00000000..3205d381 --- /dev/null +++ b/docs/backend/api/locales.md @@ -0,0 +1,47 @@ +# Locales API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Locales 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/locales/active + +- **라우트명**: `api.public.locales.active` +- **컨트롤러**: `App\Http\Controllers\Api\Public\LocaleController@active` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| locales | array | `["ko","en","fr","ja"]` | 활성 로케일 코드 배열 | +| locale_names | object | `{"ko":"한국어","en":"English","ja":"日本語","fr":"Français"}` | 로케일 코드별 표시명 맵 (config app.locale_names) | + +**에러 응답** + +_대표 에러 없음 (공개 조회). 인증·권한 미요구 엔드포인트로 도메인 특이 에러를 반환하지 않습니다._ + + + +**설명** 현재 사이트가 즉시 노출 가능한 활성 로케일 목록과 로케일별 표시명(`locale_names`) 매핑을 반환합니다. 활성 코어 언어팩을 기준으로 산출되며 인증이 필요 없는 공개 엔드포인트입니다. 언어팩 설치·활성화 직후 사용자 언어 셀렉터를 새로고침 없이 갱신하는 시나리오에 사용합니다. + + diff --git a/docs/backend/api/me.md b/docs/backend/api/me.md new file mode 100644 index 00000000..df1624b1 --- /dev/null +++ b/docs/backend/api/me.md @@ -0,0 +1,187 @@ +# Me API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Me 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### DELETE /api/me + +- **라우트명**: `api.me.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@destroy` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** + +현재 로그인한 사용자가 자신의 계정을 탈퇴한다. 프론트의 회원 탈퇴 모달(`_modal_withdraw.json`)이 호출한다. 별도 요청 파라미터 없이 인증 토큰의 사용자를 대상으로 하며, `UserService::withdrawUser()` 가 아바타·토큰 삭제와 개인정보 익명화를 수행한다. 되돌릴 수 없는 작업이므로 호출 전 사용자 확인 절차를 두는 것을 권장한다. + + +### GET /api/me + +- **라우트명**: `api.me.show` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@show` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| uuid | string | `a231747f-e82e-4cf2-9ae1-a261849dce40` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) | +| name | string | `API 문서 샘플 사용자` | 사용자 이름 | +| nickname | string | `gunwoo.oh` | 닉네임 | +| email | string | `apidoc-sample-user@example.com` | 이메일 주소 | +| avatar | null | `null` | 아바타 이미지 URL (User::getAvatarUrl() 산물, 미등록 시 null) | +| language | string | `ko` | 사용자 언어 설정 (ko: 한국어, en: 영어) | +| timezone | string | `Asia/Seoul` | 사용자 시간대 (예: Asia/Seoul, UTC) | +| country | string | `KR` | 국가 코드 (ISO 3166-1 alpha-2) | +| status | string | `active` | 계정 상태 (active: 활성, inactive: 비활성, blocked: 차단, withdrawn: 탈퇴) | +| status_label | string | `활성` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| status_variant | string | `success` | 상태 표시 색상/스타일 변형 키 (상태 Enum variant() 산물 — UI 배지용) | +| homepage | string | `https://example.com` | 홈페이지 URL | +| mobile | string | `010-9070-5662` | 휴대폰 번호 | +| phone | string | `02-805-4759` | 전화번호 | +| zipcode | string | `93153` | 우편번호 | +| address | string | `대구광역시 북구 백제고분로 720` | 기본 주소 | +| address_detail | string | `40동 835호` | 상세 주소 | +| signature | string | `Ipsam rem amet expedita est.` | 서명 | +| bio | string | `Tenetur omnis et amet omnis veniam to…` | 자기소개 | +| is_super | boolean | `false` | super 여부 | +| is_admin | boolean | `true` | 관리자 역할 보유 여부 (User::isAdmin() — 역할 관계 기반 파생) | +| withdrawn_at | null | `null` | withdrawn 일시 | +| last_login_at | string | `2026-07-05 19:15:16` | last login 일시 | +| last_login_human | string | `1일 전` | 마지막 로그인 시각의 상대 표현 (diffForHumans() 산물, 사용자 시간대 기준) | +| created_at | string | `2026-07-06 19:15:16` | 생성 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| notify_post_complete | boolean | `false` | 게시글 작성 완료 알림 수신 설정 (게시판 모듈 주입) | +| notify_post_reply | boolean | `false` | 내 게시글에 대한 답글 알림 수신 설정 (게시판 모듈 주입) | +| notify_comment | boolean | `false` | 내 게시글에 대한 댓글 알림 수신 설정 (게시판 모듈 주입) | +| notify_reply_comment | boolean | `false` | 내 댓글에 대한 대댓글 알림 수신 설정 (게시판 모듈 주입) | +| email_subscription | boolean | `false` | 광고성 이메일 수신 동의 여부 (마케팅 플러그인 주입, 채널) | +| email_subscription_at | null | `null` | email subscription 일시 | +| marketing_consent | boolean | `false` | 마케팅 정보 수신 전체 동의 마스터 키 (마케팅 플러그인 주입) | +| marketing_consent_at | null | `null` | marketing consent 일시 | +| third_party_consent | boolean | `false` | 제3자 정보 제공 동의 여부 (법적 항목, 마케팅 플러그인 주입) | +| third_party_consent_at | null | `null` | third party consent 일시 | +| info_disclosure | boolean | `false` | 개인정보 이용 안내 동의 여부 (법적 항목, 마케팅 플러그인 주입) | +| info_disclosure_at | null | `null` | info disclosure 일시 | +| marketing_consent_enabled | boolean | `true` | 마케팅 동의 항목 UI 노출 여부 (활성화 플래그) | +| marketing_consent_terms_slug | string | `marketing-terms` | 마케팅 동의에 연결된 약관 slug (미설정 시 null) | +| marketing_consent_terms_slug_set | boolean | `true` | 마케팅 동의 약관 연결 존재 여부 (프론트 링크 표시 판정용) | +| third_party_consent_enabled | boolean | `true` | 제3자 제공 동의 항목 UI 노출 여부 (활성화 플래그) | +| third_party_consent_terms_slug | null | `null` | 제3자 제공 동의에 연결된 약관 slug (미설정 시 null) | +| third_party_consent_terms_slug_set | boolean | `false` | 제3자 제공 동의 약관 연결 존재 여부 (프론트 링크 표시 판정용) | +| info_disclosure_enabled | boolean | `true` | 개인정보 이용 안내 동의 항목 UI 노출 여부 (활성화 플래그) | +| info_disclosure_terms_slug | null | `null` | 개인정보 이용 안내 동의에 연결된 약관 slug (미설정 시 null) | +| info_disclosure_terms_slug_set | boolean | `false` | 개인정보 이용 안내 동의 약관 연결 존재 여부 (프론트 링크 표시 판정용) | +| email_subscription_enabled | boolean | `true` | 이메일 수신 동의 항목 UI 노출 여부 (활성화 플래그) | +| email_subscription_terms_slug | null | `null` | 이메일 수신 동의에 연결된 약관 slug (미설정 시 null) | +| email_subscription_terms_slug_set | boolean | `false` | 이메일 수신 동의 약관 연결 존재 여부 (프론트 링크 표시 판정용) | +| channels | array | `[{"key":"email_subscription","label":"광고성 이메일 수신","enable…` | 관리자 정의 전체 마케팅 채널 목록 (원소 key/label/enabled/terms_slug, 마케팅 플러그인 주입) | +| consent_histories | array | `[]` | 동의 변경 이력 (원소 channel_key/action/source/created_at, 마케팅 플러그인 주입) | +| ecommerce_mileage | object | `{"enabled":false}` | 마일리지 정보 (enabled: 기능 활성 여부, 잔액, 이커머스 모듈 주입) | +| ecommerce_preferred_currency | null | `null` | 선호 결제 통화 (이커머스 모듈 주입, 미설정 시 null) | +| ecommerce_preferred_shipping_country | null | `null` | 선호 배송 국가 코드 (이커머스 모듈 주입, 미설정 시 null) | +| ecommerce_preferred_shipping_country_name | null | `null` | 선호 배송 국가 이름 (코드 파생, 이커머스 모듈 주입, 미설정 시 null) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** + +현재 로그인한 사용자의 프로필 정보를 조회한다. 프론트의 마이페이지(`mypage/profile.json`)와 프로필 수정 화면(`mypage/profile-edit.json`)이 소비한다. 응답은 `UserResource::toProfileArray()` 산물로, 비밀번호 등 민감 필드는 제외된다. 표의 `notify_*`(게시판 모듈)·`marketing_consent*`/`email_subscription*`/`third_party_consent*`/`info_disclosure*`/`channels`/`consent_histories`(마케팅 플러그인)·`ecommerce_*`(이커머스 모듈) 필드는 코어가 아니라 각 확장이 `core.user.filter_resource_data` 훅으로 병합하는 확장 소유 필드이며, 해당 확장이 비활성인 환경에서는 응답에 나타나지 않는다. + + +### PUT /api/me + +- **라우트명**: `api.me.update` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@update` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | string | 예 | max 255 | 대상의 이름/명칭 | +| nickname | body | string | 아니오 | max 50 | 닉네임 | +| email | body | email | 예 | max 255 | 이메일 주소 | +| password | body | string | 아니오 | — | 비밀번호 | +| current_password | body | string | 아니오 | — | 현재 비밀번호 (변경 전 확인용) | +| language | body | string | 아니오 | — | 언어 코드 | +| country | body | string | 아니오 | — | 국가 코드 (ISO 3166-1 alpha-2) | +| timezone | body | string | 아니오 | — | 타임존 식별자 | +| homepage | body | string | 아니오 | max 255 | 홈페이지 URL | +| mobile | body | string | 아니오 | max 20 | 휴대전화 번호 | +| phone | body | string | 아니오 | max 20 | 전화번호 | +| zipcode | body | string | 아니오 | max 10 | 우편번호 | +| address | body | string | 아니오 | max 255 | 기본 주소 | +| address_detail | body | string | 아니오 | max 255 | 상세 주소 | +| signature | body | string | 아니오 | max 1000 | 서명 | +| bio | body | string | 아니오 | max 5000 | 자기소개 | +| notify_post_complete | body | boolean | 아니오 | — | 게시글 작성 완료 알림 수신 설정 (게시판 모듈 추가) | +| notify_post_reply | body | boolean | 아니오 | — | 내 게시글에 대한 답글 알림 수신 설정 (게시판 모듈 추가) | +| notify_comment | body | boolean | 아니오 | — | 내 게시글에 대한 댓글 알림 수신 설정 (게시판 모듈 추가) | +| notify_reply_comment | body | boolean | 아니오 | — | 내 댓글에 대한 대댓글 알림 수신 설정 (게시판 모듈 추가) | +| email_subscription | body | boolean | 아니오 | — | 광고성 이메일 수신 동의 (마케팅 플러그인 추가, 채널) | +| marketing_consent | body | boolean | 아니오 | — | 마케팅 정보 수신 전체 동의 마스터 키 (마케팅 플러그인 추가) | +| third_party_consent | body | boolean | 아니오 | — | 제3자 정보 제공 동의 (법적 항목, 마케팅 플러그인 추가) | +| info_disclosure | body | boolean | 아니오 | — | 개인정보 이용 안내 동의 (법적 항목, 마케팅 플러그인 추가) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.update_profile_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +현재 로그인한 사용자가 자신의 프로필을 수정한다. 프론트의 프로필 수정 화면(`partials/mypage/profile/_edit.json`)이 사용한다. `name`·`email` 은 필수이며 `email` 은 본인을 제외한 중복이 허용되지 않는다. 비밀번호를 함께 변경하려면 `password`(+ `password_confirmation`)와 현재 비밀번호(`current_password`)를 함께 보내야 하며, `password` 가 빈 문자열이면 비밀번호 미변경으로 처리된다. `notify_*`·`marketing_consent`·`email_subscription` 등 확장 소유 파라미터는 게시판 모듈·마케팅 플러그인이 `core.user.update_profile_validation_rules` 훅으로 추가하며, 해당 확장이 비활성인 환경에서는 수용되지 않는다. 성공 시 갱신된 프로필이 `UserResource` 형태로 반환된다. + + diff --git a/docs/backend/api/menus.md b/docs/backend/api/menus.md new file mode 100644 index 00000000..f6fda8a0 --- /dev/null +++ b/docs/backend/api/menus.md @@ -0,0 +1,418 @@ +# Menus API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Menus 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/menus + +- **라우트명**: `api.admin.menus.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\MenuController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.menus.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| is_active | query | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| filters | query | array | 아니오 | max 10 | 추가 필터 조건 맵 (필드별 조건) | +| sort_by | query | string | 아니오 | `created_at`, `name`, `slug`, `order` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.menu.list_validation_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"대시보드","en":"Dashboard","ja":"ダッシュボード"}` | 메뉴 이름 (다국어 JSON) | +| slug | string | `admin-dashboard` | 메뉴 슬러그 | +| url | string | `/admin/dashboard` | 메뉴 URL | +| icon | string | `fas fa-tachometer-alt` | 메뉴 아이콘 | +| order | integer | `1` | 메뉴 순서 | +| is_active | boolean | `true` | active 여부 | +| parent_id | null | `null` | 상위 메뉴 ID | +| extension_type | string | `core` | 확장 소유 타입: core(코어), module(모듈), plugin(플러그인), NULL(사용자 정의) | +| extension_identifier | string | `core` | 확장 식별자 (예: core, sirsoft-board, sirsoft-payment) | +| children | array | `[]` | 하위 항목 배열 (계층 트리 — children 관계 파생) | +| creator | null | `null` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| roles | array | `[{"id":1,"name":{"ko":"관리자","en":"Administrator"},"permis…` | 이 메뉴 노출이 허용된 역할 목록 (원소 id/name/permission_type — roles 관계 파생, permission_type 은 pivot 의 노출 권한 유형) | +| created_at | string | `2026-05-27 15:20:18` | 생성 일시 | +| updated_at | string | `2026-05-27 15:21:38` | 최종 수정 일시 | +| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +관리자 메뉴 관리 화면(`partials/admin_menu_list/*`)의 목록 표시 기준 엔드포인트. `is_active` 또는 `filters` 가 있으면 필터링된 관리용 메뉴를, 없으면 최상위 메뉴 전체를 반환한다. `filters` 는 `field`(name/slug/url/all)·`value`·`operator`(like/eq/starts_with/ends_with) 조합의 배열이며 최대 10개까지 허용된다. `name` 은 다국어 JSON 객체로 내려오고, 각 항목에 `children`/`creator`/`roles` 관계와 `abilities`(현재 사용자의 수정/삭제 가능 여부)가 포함된다. 인증 계약: `auth:sanctum` + `permission:core.menus.read`. + + +### POST /api/admin/menus + +- **라우트명**: `api.admin.menus.store` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\MenuController@store` +- **인증/권한**: `auth:sanctum` + `permission:core.menus.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | string | 예 | — | 대상의 이름/명칭 | +| slug | body | string | 예 | max 255 | URL 친화 식별자 (slug) | +| url | body | string | 아니오 | max 500 | URL | +| icon | body | string | 아니오 | max 100 | 아이콘 | +| parent_id | body | integer | 아니오 | — | parent 식별자 | +| order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| extension_type | body | string | 아니오 | `core`, `module`, `plugin` | 확장 유형 (core/module/plugin/template) | +| extension_identifier | body | string | 아니오 | max 255 | 확장 식별자 | +| roles | body | array | 아니오 | — | 이 메뉴 노출을 허용할 역할 ID 배열 (각 원소는 존재하는 role id — 지정 시 노출 허용 역할 목록으로 설정/교체) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.menu.create_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +새 메뉴를 생성한다. `name` 은 다국어 값(문자열로 보내면 지원 로케일 전체에 동일 값으로 자동 확장)이며 `slug` 는 `menus` 테이블 내 유일해야 한다. `parent_id` 로 하위 메뉴를 만들 수 있고, `roles` 배열로 이 메뉴 노출을 허용할 역할 ID 를 지정한다. 성공 시 `201` 과 함께 생성된 메뉴(관계 eager-load 포함)를 `MenuResource` 로 반환한다. 관리자 메뉴 관리 화면의 메뉴 추가 폼에서 소비된다. 인증 계약: `auth:sanctum` + `permission:core.menus.create`. + + +### GET /api/admin/menus/active + +- **라우트명**: `api.admin.menus.active` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\MenuController@active` +- **인증/권한**: `auth:sanctum` + `permission:core.menus.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"대시보드","en":"Dashboard","ja":"ダッシュボード"}` | 메뉴 이름 (다국어 JSON) | +| slug | string | `admin-dashboard` | 메뉴 슬러그 | +| url | string | `/admin/dashboard` | 메뉴 URL | +| icon | string | `fas fa-tachometer-alt` | 메뉴 아이콘 | +| order | integer | `1` | 메뉴 순서 | +| is_active | boolean | `true` | active 여부 | +| children | array | `[]` | 하위 항목 배열 (계층 트리 — children 관계 파생) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.read`)이 없는 경우 | + + + +**설명** + +관리자 레이아웃 전역 부트스트랩 엔드포인트. `sirsoft-admin_basic/layouts/_admin_base.json` 의 `admin_menu` `data_source`(`/api/admin/menus/active`)가 모든 관리자 페이지 진입 시 자동 호출해, 사이드바 메뉴 트리를 채운다. 인증 사용자의 역할 기준으로 접근 가능한 활성 메뉴만 계층 구조(`children` 중첩)로 반환하며, `data` 는 최상위 메뉴 배열이다. 사용자 정보가 없을 때는 모든 활성 메뉴를 fallback 으로 반환한다. 인증 계약: `auth:sanctum` + `permission:core.menus.read`. + + +### GET /api/admin/menus/extension/{type}/{identifier} + +- **라우트명**: `api.admin.menus.by-extension` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\MenuController@getByExtension` +- **인증/권한**: `auth:sanctum` + `permission:core.menus.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| type | path | string | 예 | — | 확장 소유 타입 (ExtensionOwnerType 으로 파싱 — module: 모듈, plugin: 플러그인. 미유효 값이면 422 menu.invalid_extension_type) | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +특정 확장이 소유한 메뉴 목록을 조회한다. `type` 은 `ExtensionOwnerType`(module, plugin)으로 파싱되며, 유효하지 않은 값이면 `422 menu.invalid_extension_type` 을 반환한다. `identifier` 는 확장 식별자(예: `sirsoft-board`)로, 해당 확장이 등록한 메뉴만 `MenuCollection` 으로 내려준다. 확장 설치/제거 시 그 확장 소유 메뉴를 확인·정리하는 관리 흐름에서 사용된다. 인증 계약: `auth:sanctum` + `permission:core.menus.read`. + + +### GET /api/admin/menus/hierarchy + +- **라우트명**: `api.admin.menus.hierarchy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\MenuController@hierarchy` +- **인증/권한**: `auth:sanctum` + `permission:core.menus.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"대시보드","en":"Dashboard","ja":"ダッシュボード"}` | 메뉴 이름 (다국어 JSON) | +| slug | string | `admin-dashboard` | 메뉴 슬러그 | +| url | string | `/admin/dashboard` | 메뉴 URL | +| icon | string | `fas fa-tachometer-alt` | 메뉴 아이콘 | +| order | integer | `1` | 메뉴 순서 | +| is_active | boolean | `true` | active 여부 | +| children | array | `[]` | 하위 항목 배열 (계층 트리 — children 관계 파생) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.read`)이 없는 경우 | + + + +**설명** + +전체 메뉴를 계층 구조로 반환한다. `MenuService::getMenuHierarchy()` 로 조회한 메뉴를 `MenuCollection::toNavigationArray()` 로 변환해 최상위 메뉴 + `children` 중첩 형태로 내려준다. `active` 와 달리 사용자 역할 기반 접근 필터 없이 메뉴 트리 전체를 노출하므로, 관리자 메뉴 관리 화면의 트리 표현·순서 편집 기준 데이터로 사용된다. 인증 계약: `auth:sanctum` + `permission:core.menus.read`. + + +### PUT /api/admin/menus/order + +- **라우트명**: `api.admin.menus.update-order` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\MenuController@updateOrder` +- **인증/권한**: `auth:sanctum` + `permission:core.menus.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| parent_menus | body | array | 예 | min 1 | 최상위 메뉴의 새 순서 목록 (원소 `{id, order}` — id: 대상 메뉴, order: 1 이상 표시 순번) | +| child_menus | body | array | 아니오 | — | 부모별 하위 메뉴의 새 순서 목록 (부모 그룹핑된 2차원 배열, 각 원소 `{id, order}` — id: 하위 메뉴, order: 1 이상 표시 순번) | +| moved_items | body | array | 아니오 | — | 부모가 바뀐 항목 목록 (원소 `{id, new_parent_id}` — new_parent_id 는 새 부모 메뉴 ID 또는 null(최상위 이동), 순환 참조(자기 자신·자손 지정) 시 거부) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.menu.update_order_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +관리자 메뉴 관리 화면의 드래그 앤 드롭 순서 변경을 반영한다. `parent_menus` 는 `{id, order}` 배열로 최상위 메뉴 순서를, `child_menus` 는 부모별 하위 메뉴 순서를, `moved_items` 는 부모가 바뀐 항목(`{id, new_parent_id}`)을 전달한다. `moved_items` 의 `new_parent_id` 에는 순환 참조 방지(`NotCircularParent`) 검증이 적용되어, 자기 자신이나 자손을 부모로 지정하면 거부된다. 각 id/parent_id 는 실제 메뉴로 존재해야 한다. 응답은 성공 메시지만 반환한다. 인증 계약: `auth:sanctum` + `permission:core.menus.update`. + + +### DELETE /api/admin/menus/{menu} + +- **라우트명**: `api.admin.menus.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\MenuController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:core.menus.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| menu | path | string | 예 | — | 대상 menu의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +지정한 메뉴를 삭제한다. `{menu}` 는 라우트 모델 바인딩으로 해석되며, 삭제는 `MenuService::deleteMenu()` 가 수행한다. 성공 시 `menu.delete_success` 메시지만 반환하고, 삭제 불가 조건은 `422 menu.delete_failed` 로 내려온다. 관리자 메뉴 관리 화면의 메뉴 삭제 동작에서 소비된다. 인증 계약: `auth:sanctum` + `permission:core.menus.delete`. + + +### GET /api/admin/menus/{menu} + +- **라우트명**: `api.admin.menus.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\MenuController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.menus.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| menu | path | string | 예 | — | 대상 menu의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `33` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 메뉴","en":"API Doc Sample Menu"}` | 메뉴 이름 (다국어 JSON) | +| slug | string | `apidoc-sample-menu` | 메뉴 슬러그 | +| url | string | `/admin/apidoc-sample` | 메뉴 URL | +| icon | string | `fas fa-book` | 메뉴 아이콘 | +| order | integer | `28` | 메뉴 순서 | +| is_active | boolean | `true` | active 여부 | +| parent_id | null | `null` | 상위 메뉴 ID | +| extension_type | null | `null` | 확장 소유 타입: core(코어), module(모듈), plugin(플러그인), NULL(사용자 정의) | +| extension_identifier | null | `null` | 확장 식별자 (예: core, sirsoft-board, sirsoft-payment) | +| parent | null | `null` | 상위 항목 객체 (parent 관계 파생) | +| children | array | `[{"id":34,"name":{"ko":"하위 메뉴","en":"Child Menu"},"slug":…` | 하위 항목 배열 (계층 트리 — children 관계 파생) | +| creator | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| roles | array | `[]` | 이 메뉴 노출이 허용된 역할 목록 (원소 id/name/permission_type — roles 관계 파생, permission_type 은 pivot 의 노출 권한 유형) | +| created_at | string | `2026-07-06 19:15:16` | 생성 일시 | +| updated_at | string | `2026-07-06 19:15:16` | 최종 수정 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +단일 메뉴의 상세 정보를 조회한다. `{menu}` 는 라우트 모델 바인딩으로 해석되며, `creator`/`parent`/`children`/`roles` 관계를 eager-load 해 함께 반환한다. `children` 에는 하위 메뉴가 order 오름차순으로, 각 하위 메뉴의 `roles` 까지 포함된다. 관리자 메뉴 관리 화면의 메뉴 편집 폼 초기값 로딩에 사용된다. 인증 계약: `auth:sanctum` + `permission:core.menus.read`. + + +### PUT /api/admin/menus/{menu} + +- **라우트명**: `api.admin.menus.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\MenuController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.menus.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| menu | path | string | 예 | — | 대상 menu의 식별자 | +| name | body | string | 아니오 | — | 대상의 이름/명칭 | +| slug | body | string | 예 | max 255 | URL 친화 식별자 (slug) | +| url | body | string | 아니오 | max 500 | URL | +| icon | body | string | 아니오 | max 100 | 아이콘 | +| parent_id | body | integer | 아니오 | — | parent 식별자 | +| order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| extension_type | body | string | 아니오 | `core`, `module`, `plugin` | 확장 유형 (core/module/plugin/template) | +| extension_identifier | body | string | 아니오 | max 255 | 확장 식별자 | +| roles | body | array | 아니오 | — | 이 메뉴 노출을 허용할 역할 ID 배열 (각 원소는 존재하는 role id — 지정 시 노출 허용 역할 목록으로 설정/교체) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.menu.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +기존 메뉴 정보를 수정한다. `{menu}` 는 라우트 모델 바인딩으로 해석되며, 전달된 필드만 갱신한다. `slug` 는 여전히 유일해야 하고, `roles` 배열을 보내면 이 메뉴 노출이 허용된 역할 목록을 교체한다. 성공 시 갱신된 메뉴를 관계 eager-load 포함해 `MenuResource` 로 반환하고, 실패 시 `menu.update_failed`(422 검증 오류 포함)를 반환한다. 관리자 메뉴 관리 화면의 메뉴 편집 폼 저장에서 소비된다. 인증 계약: `auth:sanctum` + `permission:core.menus.update`. + + +### PATCH /api/admin/menus/{menu}/toggle-status + +- **라우트명**: `api.admin.menus.toggle-status` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\MenuController@toggleStatus` +- **인증/권한**: `auth:sanctum` + `permission:core.menus.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| menu | path | string | 예 | — | 대상 menu의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +지정한 메뉴의 활성화 상태(`is_active`)를 반대 값으로 토글한다. `{menu}` 는 라우트 모델 바인딩으로 해석되며, 본문 파라미터 없이 현재 상태를 뒤집는다. 성공 시 갱신된 메뉴를 `MenuResource` 로 반환한다. 관리자 메뉴 관리 화면의 활성/비활성 스위치에서 소비된다. 인증 계약: `auth:sanctum` + `permission:core.menus.update`. + + diff --git a/docs/backend/api/modules.md b/docs/backend/api/modules.md new file mode 100644 index 00000000..692ec3c7 --- /dev/null +++ b/docs/backend/api/modules.md @@ -0,0 +1,829 @@ +# Modules API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Modules 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/modules + +- **라우트명**: `api.admin.modules.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.read|core.menus.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| filters | query | array | 아니오 | max 10 | 추가 필터 조건 맵 (필드별 조건) | +| status | query | string | 아니오 | `installed`, `not_installed`, `active`, `inactive` | 상태 필터 (해당 상태의 항목만 조회) | +| with | query | array | 아니오 | max 5 | 함께 포함할 추가 데이터 옵션 목록 (허용값 `custom_menus` — 각 모듈의 커스텀 메뉴 데이터 포함) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| include_hidden | query | boolean | 아니오 | — | manifest `hidden=true` 로 표시된 숨김 확장까지 목록에 포함할지 여부 (기본 제외) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.index_validation_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `sirsoft-board` | 모듈 고유 식별자 (vendor-module 형식) | +| vendor | string | `sirsoft` | 벤더/개발자명 | +| name | string | `게시판` | 모듈 이름 (다국어 JSON) | +| version | string | `1.0.0` | 모듈 버전 | +| description | string | `게시판 관리를 위한 모듈` | 모듈 설명 (다국어 JSON) | +| dependencies | array | `[]` | 의존하는 확장 맵 (manifest 파생 — {modules, plugins}) | +| status | string | `active` | 상태 (active: 활성화, inactive: 비활성화, installing: 설치 중, uninstalling: 제거 중, updating: 업데이트 중) | +| assets | object | `{"js":"\/api\/modules\/assets\/sirsoft-ecommerce\/dist\/j…` | 프론트엔드 에셋 매니페스트 (manifest 파생 — js/css 진입점·로딩 전략) | +| update_available | boolean | `false` | 최신 버전 대비 업데이트 가능 여부 | +| update_source | null | `null` | 업데이트 감지 출처 (github, bundled 등) | +| latest_version | string | `1.0.0` | 감지된 최신 배포 버전 | +| file_version | string | `1.0.0` | 설치된 파일의 manifest 버전 | +| github_url | string | `https://github.com/gnuboard/g7-module…` | GitHub 저장소 URL | +| github_changelog_url | string | `https://github.com/gnuboard/g7-module…` | GitHub 변경 내역 URL | +| is_pending | boolean | `false` | _pending 대기소에 있어 설치 대기 중인지 여부 | +| is_bundled | boolean | `false` | 코어에 선탑재된 번들 확장인지 여부 | +| deactivated_reason | null | `null` | 비활성화 사유: manual(사용자 수동) \| incompatible_core(코어 버전 호환성) \| null(active) | +| deactivated_at | null | `null` | deactivated 일시 | +| incompatible_required_version | null | `null` | 요구 코어 버전 미충족 시 필요한 버전 (호환되면 null) | +| abilities | object | `{"can_install":true,"can_activate":true,"can_uninstall":t…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read|core.menus.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 설치된 모듈과 미설치 모듈을 모두 포함한 전체 모듈 목록을 페이지네이션으로 조회합니다. `search` 는 이름·식별자·설명·벤더에 대한 OR 검색이고 `filters` 는 AND 조건으로 적용되며, `with[]` 에 `custom_menus` 를 지정하면 커스텀 메뉴 데이터를 함께 포함합니다. `core.modules.read` 또는 `core.menus.read` 권한 중 하나가 필요하고, 응답의 `abilities` 는 현재 사용자의 수행 가능 작업 맵을 담습니다. 관리자 모듈 관리 화면의 목록 그리드를 구성하는 기본 엔드포인트입니다. + + +### POST /api/admin/modules/activate + +- **라우트명**: `api.admin.modules.activate` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@activate` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.activate` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| module_name | body | string | 예 | max 255 | module 이름 (식별자) | +| force | body | boolean | 아니오 | — | 강제 실행 여부 (안전 확인/선행 검사 우회) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.activate_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.activate`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 설치된 모듈을 활성화합니다. `core.modules.activate` 권한이 필요합니다. `force` 없이 호출했을 때 필요한 의존 확장이 충족되지 않으면 409 응답으로 `missing_modules`·`missing_plugins` 목록과 함께 경고를 반환하므로, 사용자 확인 후 `force: true` 로 재요청해야 합니다. 재활성화 시 cascade 로 함께 비활성화됐던 번들 언어팩 목록이 `pending_language_packs` 로 응답에 포함됩니다. + + +### POST /api/admin/modules/check-updates + +- **라우트명**: `api.admin.modules.check-updates` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@checkUpdates` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.install` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | + + + +**설명** 설치된 모든 모듈에 대해 GitHub·번들 소스를 조회하여 새 버전 배포 여부를 일괄 확인합니다. `core.modules.install` 권한이 필요합니다. 파라미터 없이 호출하며, 각 모듈의 업데이트 가능 여부와 감지된 최신 버전 정보를 반환합니다. 모듈 목록 화면 진입 시 업데이트 뱃지를 갱신하는 용도로 사용됩니다. + + +### POST /api/admin/modules/deactivate + +- **라우트명**: `api.admin.modules.deactivate` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@deactivate` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.activate` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| module_name | body | string | 예 | max 255 | module 이름 (식별자) | +| force | body | boolean | 아니오 | — | 강제 실행 여부 (안전 확인/선행 검사 우회) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.deactivate_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.activate`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 활성 모듈을 비활성화합니다. `core.modules.activate` 권한이 필요합니다. `force` 없이 호출했을 때 이 모듈에 의존하는 템플릿·모듈·플러그인이 있으면 409 응답으로 `dependent_templates`·`dependent_modules`·`dependent_plugins` 목록과 함께 경고를 반환합니다. 의존 관계 확인 후 `force: true` 로 강제 비활성화할 수 있습니다. + + +### POST /api/admin/modules/install + +- **라우트명**: `api.admin.modules.install` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@install` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| module_name | body | string | 예 | max 255 | module 이름 (식별자) | +| vendor_mode | body | string | 아니오 | `auto`, `composer`, `bundled` | 벤더 설치 모드 (auto/composer/bundled) | +| dependencies | body | array | 아니오 | — | 함께 설치할 의존 확장 목록 (cascade 1단계). 각 원소는 `type`(module\|plugin)·`identifier` 로 구성하며, install-preview 응답에서 사용자가 선택한 항목 | +| language_packs | body | array | 아니오 | — | 함께 설치할 번들 언어팩 식별자 목록 (cascade 2단계, best-effort). 원소는 언어팩 식별자 문자열 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.install_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** `_pending`·`_bundled` 대기소에 있는 모듈을 활성 디렉토리로 설치합니다. `core.modules.install` 권한이 필요합니다. `vendor_mode` 로 Composer 의존성 설치 방식을(auto/composer/bundled) 지정하며, 요청 본문의 `dependencies` 로 선택한 의존 확장을 먼저 설치(cascade 1단계, 실패 시 전체 중단)한 뒤 `language_packs` 로 지정한 번들 언어팩을 best-effort 로 함께 설치합니다(cascade 2단계). 성공 시 201 상태로 반환하고, 언어팩 설치 실패는 응답의 `language_pack_failures` 에 담깁니다. + + +### POST /api/admin/modules/install-from-file + +- **라우트명**: `api.admin.modules.install-from-file` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@installFromFile` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| file | body | file | 예 | max 51200 | 업로드 파일 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.install_from_file_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 업로드된 ZIP 파일에서 모듈을 설치합니다. `core.modules.install` 권한이 필요하며, 파일은 최대 50MB(51200KB)까지 허용됩니다. ZIP 압축 해제 후 module.json 검증을 거쳐 설치하며, 성공 시 201 상태로 설치된 모듈 정보를 반환합니다. 설치 전 manifest 만 미리 확인하려면 `manifest-preview` 를 먼저 호출하는 것이 안전합니다. + + +### POST /api/admin/modules/install-from-github + +- **라우트명**: `api.admin.modules.install-from-github` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@installFromGithub` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| github_url | body | string | 예 | — | GitHub 저장소 URL | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.install_from_github_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** GitHub 저장소 URL 에서 모듈을 내려받아 설치합니다. `core.modules.install` 권한이 필요합니다. `github_url` 로 지정한 공개 저장소의 릴리스/소스를 받아 압축 해제·검증 후 설치하며, 성공 시 201 상태로 설치된 모듈 정보를 반환합니다. + + +### GET /api/admin/modules/installed + +- **라우트명**: `api.admin.modules.installed` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@installed` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `sirsoft-board` | 모듈 고유 식별자 (vendor-module 형식) | +| vendor | string | `sirsoft` | 벤더/개발자명 | +| name | string | `게시판` | 모듈 이름 (다국어 JSON) | +| version | string | `1.0.0` | 모듈 버전 | +| description | string | `게시판 관리를 위한 모듈` | 모듈 설명 (다국어 JSON) | +| dependencies | array | `[]` | 의존하는 확장 맵 (manifest 파생 — {modules, plugins}) | +| status | string | `active` | 상태 (active: 활성화, inactive: 비활성화, installing: 설치 중, uninstalling: 제거 중, updating: 업데이트 중) | +| assets | object | `{"js":"\/api\/modules\/assets\/sirsoft-ecommerce\/dist\/j…` | 프론트엔드 에셋 매니페스트 (manifest 파생 — js/css 진입점·로딩 전략) | +| update_available | boolean | `false` | 최신 버전 대비 업데이트 가능 여부 | +| update_source | null | `null` | 업데이트 감지 출처 (github, bundled 등) | +| latest_version | string | `1.0.0` | 감지된 최신 배포 버전 | +| file_version | string | `1.0.0` | 설치된 파일의 manifest 버전 | +| github_url | string | `https://github.com/gnuboard/g7-module…` | GitHub 저장소 URL | +| github_changelog_url | string | `https://github.com/gnuboard/g7-module…` | GitHub 변경 내역 URL | +| is_pending | boolean | `false` | _pending 대기소에 있어 설치 대기 중인지 여부 | +| is_bundled | boolean | `false` | 코어에 선탑재된 번들 확장인지 여부 | +| deactivated_reason | null | `null` | 비활성화 사유: manual(사용자 수동) \| incompatible_core(코어 버전 호환성) \| null(active) | +| deactivated_at | null | `null` | deactivated 일시 | +| incompatible_required_version | null | `null` | 요구 코어 버전 미충족 시 필요한 버전 (호환되면 null) | +| abilities | object | `{"can_install":true,"can_activate":true,"can_uninstall":t…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 현재 설치된 모듈만 조회합니다(미설치 항목 제외). 이 엔드포인트는 세부 권한 미들웨어 없이 `auth:sanctum` 인증만 요구하므로, 다른 화면이 활성/설치된 모듈 목록을 참조할 때 사용하는 경량 조회 API 입니다. 페이지네이션 없이 설치된 항목 배열을 반환합니다. + + +### POST /api/admin/modules/manifest-preview + +- **라우트명**: `api.admin.modules.manifest-preview` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@manifestPreview` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| file | body | file | 예 | max 51200 | 업로드 파일 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 업로드된 ZIP 파일의 module.json manifest 와 검증 결과만 추출합니다(실제 설치는 수행하지 않음). `core.modules.install` 권한이 필요하며 파일은 최대 50MB 까지 허용됩니다. 설치 모달에서 사용자가 파일 선택 직후 manifest 유효성과 검증 실패 사유를 미리 확인하는 용도이며, 검증 오류 시 422 로 사유를 반환합니다. + + +### POST /api/admin/modules/refresh-layouts + +- **라우트명**: `api.admin.modules.refresh-layouts` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@refreshLayouts` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.activate` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| module_name | body | string | 예 | max 255 | module 이름 (식별자) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.refresh_layouts_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.activate`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 모듈의 레이아웃 파일을 파일에서 다시 읽어 DB 에 동기화합니다. `core.modules.activate` 권한이 필요합니다. 파일에서 변경된 레이아웃은 갱신되고 삭제된 레이아웃은 DB 에서도 제거되며, 갱신된 모듈 정보를 반환합니다. 모듈의 `_bundled` 레이아웃 JSON 을 수정한 뒤 재빌드 없이 반영할 때 사용합니다. + + +### DELETE /api/admin/modules/uninstall + +- **라우트명**: `api.admin.modules.uninstall` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@uninstall` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.uninstall` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| module_name | query | string | 예 | max 255 | module 이름 (식별자) | +| delete_data | query | boolean | 아니오 | — | 제거 시 모듈이 생성한 DB 데이터까지 함께 삭제할지 여부 (기본 false — 데이터 보존) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.uninstall_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.uninstall`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 모듈을 시스템에서 제거합니다. `core.modules.uninstall` 권한이 필요합니다. 활성 디렉토리만 삭제하고 `_bundled` 원본은 보존합니다. `delete_data: true` 인 경우 모듈이 생성한 DB 데이터까지 함께 삭제하며, 기본값은 데이터 보존입니다. 삭제될 데이터 범위는 사전에 `uninstall-info` 로 확인할 수 있습니다. + + +### GET /api/admin/modules/uninstalled + +- **라우트명**: `api.admin.modules.uninstalled` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@uninstalled` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `gnuboard7-hello_module` | 모듈 고유 식별자 (vendor-module 형식) | +| vendor | string | `gnuboard7` | 벤더/개발자명 | +| name | string | `Hello 모듈` | 모듈 이름 (다국어 JSON) | +| version | string | `0.1.0` | 모듈 버전 | +| description | string | `학습용 최소 샘플 모듈 (Memo CRUD)` | 모듈 설명 (다국어 JSON) | +| dependencies | array | `[]` | 의존하는 확장 맵 (manifest 파생 — {modules, plugins}) | +| status | string | `uninstalled` | 상태 (active: 활성화, inactive: 비활성화, installing: 설치 중, uninstalling: 제거 중, updating: 업데이트 중) | +| assets | null | `null` | 프론트엔드 에셋 매니페스트 (manifest 파생 — js/css 진입점·로딩 전략) | +| update_available | boolean | `false` | 최신 버전 대비 업데이트 가능 여부 | +| update_source | null | `null` | 업데이트 감지 출처 (github, bundled 등) | +| latest_version | null | `null` | 감지된 최신 배포 버전 | +| file_version | null | `null` | 설치된 파일의 manifest 버전 | +| github_url | null | `null` | GitHub 저장소 URL | +| github_changelog_url | null | `null` | GitHub 변경 내역 URL | +| is_pending | boolean | `false` | _pending 대기소에 있어 설치 대기 중인지 여부 | +| is_bundled | boolean | `true` | 코어에 선탑재된 번들 확장인지 여부 | +| deactivated_reason | null | `null` | 비활성화 사유: manual(사용자 수동) \| incompatible_core(코어 버전 호환성) \| null(active) | +| deactivated_at | null | `null` | deactivated 일시 | +| incompatible_required_version | null | `null` | 요구 코어 버전 미충족 시 필요한 버전 (호환되면 null) | +| abilities | object | `{"can_install":true,"can_activate":true,"can_uninstall":t…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read`)이 없는 경우 | + + + +**설명** 아직 설치되지 않은 모듈만 조회합니다(예: 번들로 제공되나 미설치 상태인 샘플 모듈). `core.modules.read` 권한이 필요합니다. 설치 가능한 모듈을 사용자에게 노출하는 화면에서 사용하며, 미설치 항목은 assets·latest_version 등 설치 후에만 채워지는 필드가 null 로 반환됩니다. + + +### GET /api/admin/modules/{identifier}/changelog + +- **라우트명**: `api.admin.modules.changelog` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@changelog` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| source | query | string | 아니오 | `active`, `bundled`, `github` | 변경 내역 조회 출처 (active: 활성 설치본, bundled: 번들 원본, github: 원격 저장소) | +| from_version | query | string | 아니오 | — | 시작 버전 (범위 하한) | +| to_version | query | string | 아니오 | — | 대상 버전 (범위 상한) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.extension.changelog_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 모듈의 변경 내역(CHANGELOG)을 조회합니다. `core.modules.read` 권한이 필요합니다. `source` 로 조회 출처를(active: 활성 설치본, bundled: 번들 원본, github: 원격 저장소) 선택하고, `from_version`·`to_version` 으로 버전 구간을 좁힐 수 있습니다. 업데이트 전 사용자에게 변경 사항을 안내하는 데 사용됩니다. + + +### GET /api/admin/modules/{identifier}/dependent-templates + +- **라우트명**: `api.admin.modules.dependent-templates` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@dependentTemplates` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 이 모듈에 의존하는 템플릿 목록을 조회합니다. `core.modules.read` 권한이 필요합니다. 응답으로 의존 템플릿 배열과 총 개수를 반환하며, 모듈 비활성화·제거 전 영향을 받는 템플릿을 사용자에게 미리 알리는 데 사용됩니다. + + +### GET /api/admin/modules/{identifier}/license + +- **라우트명**: `api.admin.modules.license` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@license` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 모듈에 포함된 라이선스 파일의 원문 내용을 반환합니다. `core.modules.read` 권한이 필요합니다. `identifier` 는 소문자·숫자·하이픈·언더스코어 형식만 허용되며 형식에 맞지 않거나 라이선스 파일이 없으면 404 를 반환합니다. 라이선스 고지 화면에 전문을 표시하는 용도입니다. + + +### GET /api/admin/modules/{moduleName} + +- **라우트명**: `api.admin.modules.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| moduleName | path | string | 예 | — | 대상 module의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 모듈의 상세 정보를 조회합니다. `core.modules.read` 권한이 필요합니다. 목록보다 자세한 `toDetailArray()` 형태를 반환하며, 이 모듈이 지원하는 번들 언어팩 정보가 함께 주입됩니다. 모듈을 찾을 수 없으면 404 를 반환합니다. + + +### GET /api/admin/modules/{moduleName}/check-modified-layouts + +- **라우트명**: `api.admin.modules.check-modified-layouts` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@checkModifiedLayouts` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| moduleName | path | string | 예 | — | 대상 module의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 모듈에서 사용자가 수정한 레이아웃이 있는지 확인합니다. `core.modules.read` 권한이 필요합니다. 업데이트 실행 전 이 정보를 조회하여 레이아웃 전략(overwrite: 새 버전으로 교체, keep: 사용자 수정본 유지) 선택을 안내하는 데 사용됩니다. + + +### GET /api/admin/modules/{moduleName}/install-preview + +- **라우트명**: `api.admin.modules.install-preview` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@installPreview` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| moduleName | path | string | 예 | — | 대상 module의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 모듈 설치 시 함께 처리될 cascade 후보(의존 확장 + 동반 가능한 번들 언어팩) 트리를 반환합니다. `core.modules.install` 권한이 필요합니다. 설치 모달 오픈 시 호출되어 사용자가 함께 설치할 항목을 선택하도록 노출하며, ZIP 업로드 기반의 `manifest-preview` 와 달리 이미 알려진 식별자에 대한 GET 조회입니다. + + +### GET /api/admin/modules/{moduleName}/uninstall-info + +- **라우트명**: `api.admin.modules.uninstall-info` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@uninstallInfo` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.uninstall` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| moduleName | path | string | 예 | — | 대상 module의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.uninstall`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 모듈 제거 시 삭제될 데이터 정보를 조회합니다. `core.modules.uninstall` 권한이 필요합니다. 제거 확인 모달에서 사용자에게 어떤 데이터가 사라지는지 미리 보여주는 용도이며, 모듈을 찾을 수 없으면 404 를 반환합니다. + + +### POST /api/admin/modules/{moduleName}/update + +- **라우트명**: `api.admin.modules.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ModuleController@performUpdate` +- **인증/권한**: `auth:sanctum` + `permission:core.modules.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| moduleName | path | string | 예 | — | 대상 module의 이름 (식별자) | +| layout_strategy | body | string | 아니오 | `overwrite`, `keep` | 업데이트 시 레이아웃 처리 전략 (overwrite: 새 버전으로 교체, keep: 사용자 수정본 유지) | +| vendor_mode | body | string | 아니오 | `auto`, `composer`, `bundled` | 벤더 설치 모드 (auto/composer/bundled) | +| force | body | boolean | 아니오 | — | 강제 실행 여부 (안전 확인/선행 검사 우회) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.module.perform_update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 모듈을 최신 버전으로 업데이트합니다. `core.modules.install` 권한이 필요합니다. `layout_strategy` 로 레이아웃 처리 방식을(overwrite: 새 버전으로 교체, keep: 사용자 수정본 유지) 지정하며, `vendor_mode` 로 Composer 의존성 처리 방식을 선택합니다. 버전 제약·호환성 문제로 막힐 경우 `force: true` 로 강제 진행할 수 있습니다. + + +### GET /api/modules/assets/{identifier}/{path} + +- **라우트명**: `api.public.modules.assets` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicModuleController@serveAsset` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| path | path | string | 예 | — | 경로 | +| identifier | query | string | 예 | — | 대상 확장/리소스의 식별자 | +| path | query | string | 예 | — | 경로 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 모듈의 개별 프론트엔드 에셋 파일(JS/CSS/이미지 등)을 서빙하는 공개 엔드포인트입니다. 인증이 필요하지 않으며, 경로·확장자 보안 검증은 FormRequest 에서 완료됩니다. 모듈 미존재·파일 미존재·허용되지 않은 파일 유형은 각각 404/404/403 으로 응답하고, 정상 파일은 ETag 와 1년 캐시 헤더를 붙여 반환합니다. 소스맵 등 개별 에셋을 직접 참조할 때 사용되며, 통합 로딩은 `bundle.js`/`bundle.css` 를 사용합니다. + + +### GET /api/modules/bundle.css + +- **라우트명**: `api.public.modules.bundle.css` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicModuleController@serveBundleCss` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +_대표 에러 없음 (공개 조회). 활성 모듈 에셋이 없어도 빈 200 응답을 반환하므로 404 를 내지 않습니다._ + + + +**설명** 활성 모듈들의 프론트엔드 CSS 를 서버에서 하나로 병합한 번들을 서빙하는 공개 엔드포인트입니다. 인증이 필요하지 않습니다. 활성 global 모듈 에셋이 없으면 빈 200(text/css) 응답을 반환하고, 있으면 병합 파일을 ETag·환경별 Cache-Control 과 함께 서빙합니다. 페이지가 모듈 스타일을 요청 1건으로 로드하도록 합니다. + + +### GET /api/modules/bundle.js + +- **라우트명**: `api.public.modules.bundle.js` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicModuleController@serveBundleJs` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +_대표 에러 없음 (공개 조회). 활성 모듈 에셋이 없어도 빈 200 응답을 반환하므로 404 를 내지 않습니다._ + + + +**설명** 활성 모듈들의 프론트엔드 IIFE JS 를 서버에서 하나로 병합한 번들을 서빙하는 공개 엔드포인트입니다. 인증이 필요하지 않습니다. 활성 global 모듈 에셋이 없으면 빈 200(text/javascript) 응답을 반환하고(프론트는 빈 스크립트 로드로 무해), 있으면 병합 파일을 ETag·환경별 Cache-Control 과 함께 서빙합니다. 프론트는 `G7Config.bundleUrls` 를 읽어 이 번들을 로드합니다. + + +### GET /api/modules/{identifier}/components.json + +- **라우트명**: `api.public.modules.components` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicModuleController@serveComponents` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 모듈의 컴포넌트 정의 파일(components.json)을 서빙하는 공개 엔드포인트입니다. 인증이 필요하지 않습니다. 편집 모드 부팅 시 ComponentRegistry 가 활성 확장 매니페스트를 네임스페이스 병합하기 위해 fetch 하며, 구버전 모듈처럼 파일이 없으면 빈 components 로 폴백합니다. 응답은 1시간 캐시됩니다. 모듈 미존재 시 404. + + +### GET /api/modules/{identifier}/editor-spec + +- **라우트명**: `api.public.modules.editor_spec` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicModuleController@serveEditorSpec` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 모듈의 레이아웃 편집기 스펙(editor-spec.json)을 서빙하는 공개 엔드포인트입니다. 인증이 필요하지 않습니다. 활성 모듈만 대상으로 하며 활성 디렉토리 → `_bundled` 폴백 순으로 읽어 `data.spec` 형태로 반환합니다. 비활성·미존재 모듈은 404 이고, 편집기 스펙 파일을 작성하지 않은 경우 spec=null 로 정상 응답합니다. + + diff --git a/docs/backend/api/notification-channels.md b/docs/backend/api/notification-channels.md new file mode 100644 index 00000000..f761bd7e --- /dev/null +++ b/docs/backend/api/notification-channels.md @@ -0,0 +1,49 @@ +# Notification Channels API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Notification Channels 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/notification-channels + +- **라우트명**: `api.admin.notification-channels.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationChannelController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `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` 훅으로 추가된 확장 채널이 병합됩니다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | + + + +**설명** 시스템에서 사용 가능한 알림 채널 목록을 반환하며, 각 채널에 설정 완료 여부(`readiness`) 정보를 붙여 제공합니다. 인증(`auth:sanctum`)과 `core.settings.read` 권한이 필요합니다. 플러그인이 Filter 훅으로 채널을 확장할 수 있으므로 목록은 설치된 확장에 따라 달라집니다. 알림 정의·템플릿 편집 화면에서 채널 선택 옵션을 채우고 미설정 채널을 안내할 때 사용합니다. + + diff --git a/docs/backend/api/notification-definitions.md b/docs/backend/api/notification-definitions.md new file mode 100644 index 00000000..a7a2acc9 --- /dev/null +++ b/docs/backend/api/notification-definitions.md @@ -0,0 +1,218 @@ +# Notification Definitions API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Notification Definitions 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/notification-definitions + +- **라우트명**: `api.admin.notification-definitions.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationDefinitionController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| extension_type | query | string | 아니오 | `core`, `module`, `plugin` | 확장 유형 (core/module/plugin/template) | +| extension_identifier | query | string | 아니오 | max 100 | 확장 식별자 | +| channel | query | string | 아니오 | max 50 | 채널 필터 — 활성 채널(`channels`) 배열에 이 채널을 포함하는 정의만 조회 (mail, database 등) | +| is_active | query | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| sort_by | query | string | 아니오 | `id`, `type`, `extension_type`, `is_active`, `created_at`, `updated_at` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification_definition.filter_index_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `1` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `1` | 기본 키 (내부 식별자) | +| type | string | `welcome` | 알림 타입 (welcome, order_confirmed 등) | +| hook_prefix | string | `core.auth` | 훅 접두사 (core.auth, sirsoft-ecommerce 등) | +| extension_type | string | `core` | 확장 타입: core, module, plugin | +| extension_identifier | string | `core` | 확장 식별자: core, sirsoft-board 등 | +| name | object | `{"ko":"회원가입 환영","en":"Welcome","ja":"会員登録 ウェルカム"}` | 다국어 이름 ({"ko": "회원가입 환영", "en": "Welcome"}) | +| description | object | `{"ko":"회원가입 완료 시 발송되는 환영 알림","en":"Welcome notification s…` | 다국어 설명 | +| variables | array | `[{"key":"name","description":"수신자 이름"},{"key":"app_name",…` | 사용 가능 변수 메타데이터 ([{key, description}]) | +| channels | array | `["mail","database"]` | 활성 채널 (["mail", "database"]) | +| hooks | array | `["core.auth.after_register"]` | 트리거 훅 목록 (["core.auth.after_register"]) | +| is_active | boolean | `true` | active 여부 | +| is_default | boolean | `true` | default 여부 | +| templates | array | `[{"id":1,"definition_id":1,"channel":"mail","subject":{"k…` | 채널별 알림 템플릿 목록 (templates 관계 로드 시 NotificationTemplateResource 배열, 미로드 시 null) | +| created_at | string | `2026-05-27 15:20:18` | 생성 일시 | +| updated_at | string | `2026-06-30 13:33:16` | 최종 수정 일시 | +| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 등록된 알림 정의 목록을 페이지네이션으로 조회합니다. 인증(`auth:sanctum`)과 `core.settings.read` 권한이 필요합니다. `search`, `extension_type`, `extension_identifier`, `channel`, `is_active` 로 필터링하고 `sort_by`/`sort_order` 로 정렬하며, 확장이 `core.notification_definition.filter_index_rules` 훅으로 필터를 추가할 수 있습니다. 관리자 알림 정의 관리 목록 화면을 렌더링할 때 사용합니다. + + +### GET /api/admin/notification-definitions/{definition} + +- **라우트명**: `api.admin.notification-definitions.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationDefinitionController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| definition | path | string | 예 | — | 대상 definition의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `21` | 기본 키 (내부 식별자) | +| type | string | `apidoc-sample.event` | 알림 타입 (welcome, order_confirmed 등) | +| hook_prefix | string | `core` | 훅 접두사 (core.auth, sirsoft-ecommerce 등) | +| extension_type | string | `core` | 확장 타입: core, module, plugin | +| extension_identifier | string | `` | 확장 식별자: core, sirsoft-board 등 | +| name | object | `{"ko":"API 문서 샘플 알림","en":"API Doc Sample Notification"}` | 다국어 이름 ({"ko": "회원가입 환영", "en": "Welcome"}) | +| description | object | `{"ko":"문서 실측용 알림 정의","en":"Sample notification"}` | 다국어 설명 | +| variables | array | `[]` | 사용 가능 변수 메타데이터 ([{key, description}]) | +| channels | array | `["database","mail"]` | 활성 채널 (["mail", "database"]) | +| hooks | array | `[]` | 트리거 훅 목록 (["core.auth.after_register"]) | +| is_active | boolean | `true` | active 여부 | +| is_default | boolean | `false` | default 여부 | +| templates | array | `[{"id":41,"definition_id":21,"channel":"mail","subject":"…` | 채널별 알림 템플릿 목록 (templates 관계 로드 시 NotificationTemplateResource 배열, 미로드 시 null) | +| created_at | string | `2026-07-06 19:15:16` | 생성 일시 | +| updated_at | string | `2026-07-06 19:15:16` | 최종 수정 일시 | +| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 단일 알림 정의의 상세 정보를 조회하며, 응답에 소속 템플릿(`templates`)을 함께 로드합니다. 인증(`auth:sanctum`)과 `core.settings.read` 권한이 필요합니다. `definition` 경로 파라미터로 대상을 지정하며, 정의 편집 화면 진입 시 채널별 템플릿을 포함한 전체 구성을 불러올 때 사용합니다. + + +### PUT /api/admin/notification-definitions/{definition} + +- **라우트명**: `api.admin.notification-definitions.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationDefinitionController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| definition | path | string | 예 | — | 대상 definition의 식별자 | +| channels | body | array | 아니오 | min 1 | 활성 채널 목록 — 이 정의가 발송에 사용할 채널 배열 (각 원소 최대 50자, mail·database 등). 지정 시 최소 1개 필요 | +| hooks | body | array | 아니오 | — | 트리거 훅 목록 — 이 알림을 발송시키는 훅 이름 배열 (각 원소 최대 255자, core.auth.after_register 등) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification_definition.filter_update_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 알림 정의의 활성 채널(`channels`), 트리거 훅(`hooks`), 활성 상태(`is_active`)를 수정합니다. 인증(`auth:sanctum`)과 `core.settings.update` 권한이 필요합니다. Service 계층에서 수정 후 템플릿을 다시 로드해 반환하며, 확장이 `core.notification_definition.filter_update_rules` 훅으로 추가 파라미터를 검증에 넣을 수 있습니다. 발송 채널 구성이나 훅 연결을 변경할 때 사용합니다. + + +### POST /api/admin/notification-definitions/{definition}/reset + +- **라우트명**: `api.admin.notification-definitions.reset` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationDefinitionController@reset` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| definition | path | string | 예 | — | 대상 definition의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 알림 정의에 속한 모든 채널 템플릿을 기본값(default) 데이터로 일괄 복원하고, 정의 자체를 default 상태로 표시합니다. 인증(`auth:sanctum`)과 `core.settings.update` 권한이 필요합니다. 각 템플릿의 제목·본문을 기본값으로 덮어쓰는 파괴적 작업이므로 사용자 편집분이 사라집니다. 관리자가 커스터마이징한 알림 문구를 초기 상태로 되돌릴 때 사용합니다. + + +### PATCH /api/admin/notification-definitions/{definition}/toggle-active + +- **라우트명**: `api.admin.notification-definitions.toggle-active` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationDefinitionController@toggleActive` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| definition | path | string | 예 | — | 대상 definition의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 알림 정의의 활성 상태(`is_active`)를 현재 값의 반대로 토글합니다. 인증(`auth:sanctum`)과 `core.settings.update` 권한이 필요합니다. 비활성 정의는 해당 알림 발송이 중단되므로, 관리자가 목록에서 특정 알림을 켜거나 끄는 스위치 조작에 사용합니다. + + diff --git a/docs/backend/api/notification-logs.md b/docs/backend/api/notification-logs.md new file mode 100644 index 00000000..dcf9dcb1 --- /dev/null +++ b/docs/backend/api/notification-logs.md @@ -0,0 +1,141 @@ +# Notification Logs API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Notification Logs 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/notification-logs + +- **라우트명**: `api.admin.notification-logs.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationLogController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.notification-logs.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| sender_user_id | query | integer | 아니오 | — | sender user 식별자 | +| recipient_user_id | query | integer | 아니오 | — | recipient user 식별자 | +| search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| channel | query | string | 아니오 | max 50 | 발송 채널 필터 (해당 채널로 발송된 이력만 조회 — mail, database, fcm 등) | +| notification_type | query | string | 아니오 | max 100 | 알림 타입 필터 (해당 타입의 이력만 조회 — welcome, order_confirmed 등) | +| extension_type | query | string | 아니오 | `core`, `module`, `plugin` | 확장 유형 (core/module/plugin/template) | +| status | query | string | 아니오 | — | 상태 필터 (해당 상태의 항목만 조회) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| sort_by | query | string | 아니오 | `id`, `channel`, `notification_type`, `status`, `sent_at`, `created_at` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification_log.filter_index_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `677` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `677` | 기본 키 (내부 식별자) | +| channel | string | `mail` | 채널: mail, database, fcm 등 | +| notification_type | string | `apidoc.sample.event` | 알림 타입: welcome, order_confirmed 등 | +| extension_type | string | `core` | 확장 타입: core, module, plugin | +| extension_identifier | string | `` | 확장 식별자 | +| recipient_user_id | integer | `166` | recipient user 식별자 (연관 리소스 참조) | +| recipient_identifier | string | `apidoc-sample-user@example.com` | 수신자 식별자 (채널별: 이메일, 디바이스토큰, user_id 등) | +| recipient_name | string | `API 문서 샘플 사용자` | 수신자 표시명 (발송 시점 스냅샷) | +| sender_user_id | integer | `166` | sender user 식별자 (연관 리소스 참조) | +| sender | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 발신자 사용자 객체 (uuid/name/email — senderUser 관계 파생) | +| recipient | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 수신자 사용자 객체 (uuid/name/email — recipientUser 관계 파생) | +| subject | string | `API 문서 샘플 알림` | 렌더링된 제목 | +| body | string | `문서 실측용 알림 본문입니다.` | 렌더링된 본문 | +| status | string | `sent` | 상태: sent, failed, skipped | +| error_message | string | `해당 채널은 비회원 발송을 허용하지 않아 발송을 건너뛰었습니다.` | 에러 메시지 | +| source | string | `apidoc` | 발송 출처: notification, test_mail 등 | +| sent_at | string | `2026-07-06 18:20:23` | sent 일시 | +| created_at | string | `2026-07-06 19:20:23` | 생성 일시 | +| updated_at | string | `2026-07-06 19:20:23` | 최종 수정 일시 | +| abilities | object | `{"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.notification-logs.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 알림 발송 이력을 페이지네이션으로 조회합니다. 인증(`auth:sanctum`)과 `core.notification-logs.read` 권한이 필요합니다. 발송자/수신자 ID, `search`, `channel`, `notification_type`, `extension_type`, `status` 로 필터링하고 `sort_by`/`sort_order` 로 정렬하며, 확장이 `core.notification_log.filter_index_rules` 훅으로 필터를 추가할 수 있습니다. 요청 사용자(`request->user()`)를 Service 에 전달해 열람 범위를 결정하며, 관리자 알림 발송 이력 화면을 렌더링할 때 사용합니다. + + +### POST /api/admin/notification-logs/bulk-delete + +- **라우트명**: `api.admin.notification-logs.bulk-destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationLogController@bulkDestroy` +- **인증/권한**: `auth:sanctum` + `permission:core.notification-logs.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.notification-logs.delete`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 알림 발송 이력을 ID 배열(`ids`)로 다건 삭제하고 삭제 건수(`deleted_count`)를 반환합니다. 인증(`auth:sanctum`)과 `core.notification-logs.delete` 권한이 필요합니다. 복구 불가능한 삭제이므로 주의가 필요하며, 관리자가 목록에서 여러 이력을 선택해 일괄 정리할 때 사용합니다. + + +### DELETE /api/admin/notification-logs/{notificationLog} + +- **라우트명**: `api.admin.notification-logs.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationLogController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:core.notification-logs.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| notificationLog | path | string | 예 | — | 대상 notification log의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.notification-logs.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 단일 알림 발송 이력을 삭제합니다. 인증(`auth:sanctum`)과 `core.notification-logs.delete` 권한이 필요합니다. `notificationLog` 경로 파라미터로 대상을 지정하며, 복구 불가능한 삭제입니다. 관리자가 개별 발송 이력 한 건을 제거할 때 사용합니다. + + diff --git a/docs/backend/api/notification-templates.md b/docs/backend/api/notification-templates.md new file mode 100644 index 00000000..da70ef44 --- /dev/null +++ b/docs/backend/api/notification-templates.md @@ -0,0 +1,146 @@ +# Notification Templates API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Notification Templates 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### POST /api/admin/notification-templates/preview + +- **라우트명**: `api.admin.notification-templates.preview` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationTemplateController@preview` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| definition_id | body | integer | 예 | — | definition 식별자 | +| subject | body | array | 예 | — | 제목 | +| body | body | array | 예 | — | 본문 | +| locale | body | string | 아니오 | max 10 | 로케일 코드 (표시 언어/지역) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 저장 전 알림 템플릿의 렌더링 결과를 미리 확인합니다. `definition_id` 와 다국어 `subject`/`body`, 선택적 `locale` 을 받아 샘플 변수로 치환된 제목·본문을 반환합니다. 인증(`auth:sanctum`)과 `core.settings.read` 권한이 필요하며, 실제 발송이나 저장은 일어나지 않습니다. 템플릿 편집 화면에서 변수 치환 결과를 실시간으로 확인할 때 사용합니다. + + +### PUT /api/admin/notification-templates/{template} + +- **라우트명**: `api.admin.notification-templates.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationTemplateController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| template | path | string | 예 | — | 대상 template의 식별자 | +| subject | body | array | 예 | — | 제목 | +| body | body | array | 예 | — | 본문 | +| click_url | body | string | 아니오 | max 500 | 알림 클릭 시 이동할 대상 URL (미설정 시 이동 없음) | +| recipients | body | array | 아니오 | — | 수신자 규칙 목록. 각 원소는 type(trigger_user: 이벤트 유발 사용자, related_user: 연관 사용자, role: 역할 대상, specific_users: 지정 사용자), value(대상 식별값), relation(연관 사용자 관계명), exclude_trigger_user(유발 사용자 제외 여부)로 구성 | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification_template.filter_update_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 단일 채널 알림 템플릿의 다국어 제목(`subject`)·본문(`body`)과 클릭 URL, 수신자(`recipients`), 활성 상태를 수정합니다. 인증(`auth:sanctum`)과 `core.settings.update` 권한이 필요합니다. `template` 경로 파라미터로 대상을 지정하며, 확장이 `core.notification_template.filter_update_rules` 훅으로 추가 파라미터를 검증에 넣을 수 있습니다. 관리자가 특정 채널의 알림 문구를 편집해 저장할 때 사용합니다. + + +### POST /api/admin/notification-templates/{template}/reset + +- **라우트명**: `api.admin.notification-templates.reset` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationTemplateController@reset` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| template | path | string | 예 | — | 대상 template의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 단일 채널 템플릿을 소속 정의의 기본값 데이터로 복원합니다. 인증(`auth:sanctum`)과 `core.settings.update` 권한이 필요합니다. 소속 정의가 없으면 404, 해당 채널의 기본 데이터가 없으면 404 를 반환합니다. 편집한 문구를 버리고 기본값 하나만 되돌릴 때 사용하며, 정의 전체를 복원하는 정의 reset 과 달리 대상 템플릿에만 적용됩니다. + + +### PATCH /api/admin/notification-templates/{template}/toggle-active + +- **라우트명**: `api.admin.notification-templates.toggle-active` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationTemplateController@toggleActive` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| template | path | string | 예 | — | 대상 template의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 단일 채널 알림 템플릿의 활성 상태(`is_active`)를 현재 값의 반대로 토글합니다. 인증(`auth:sanctum`)과 `core.settings.update` 권한이 필요합니다. 비활성 템플릿은 해당 채널로의 발송이 중단되므로, 정의는 유지한 채 특정 채널만 켜거나 끌 때 사용합니다. + + diff --git a/docs/backend/api/notifications.md b/docs/backend/api/notifications.md new file mode 100644 index 00000000..42c0855c --- /dev/null +++ b/docs/backend/api/notifications.md @@ -0,0 +1,454 @@ +# Notifications API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Notifications 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/notifications + +- **라우트명**: `api.admin.notifications.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.notifications.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| read | query | string | 아니오 | `unread`, `read`, `all` | 읽음 상태 필터 (unread: 미읽음(`read_at` null)만, read: 읽음(`read_at` not null)만, all: 전체). 미지정 시 전체 | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification.filter_index_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +인증된 관리자 본인의 사이트내 알림 목록을 최신순(`created_at desc`)으로 페이지네이션해 반환합니다. `read` 파라미터로 미읽음(`unread`)·읽음(`read`)·전체(`all`, 기본)를 필터링하고, `per_page` 미지정 시 15건 단위로 반환합니다. 조회 대상은 항상 요청 사용자 본인의 알림으로 한정되며, 다른 사용자의 알림은 조회되지 않습니다. `_admin_base.json` 헤더의 알림 벨이 이 엔드포인트를 auto_fetch 로 소비하며 WebSocket 알림 수신 시 갱신됩니다. 항목 필드는 `id`, `type`, `type_label`, `subject`, `body`, `url`, `read_at`, `created_at` 로 구성됩니다. + + +### DELETE /api/admin/notifications/all + +- **라우트명**: `api.admin.notifications.destroy-all` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationController@destroyAll` +- **인증/권한**: `auth:sanctum` + `permission:core.notifications.delete` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.notifications.delete`)이 없는 경우 | + + + +**설명** + +인증된 관리자 본인의 모든 사이트내 알림(읽음·미읽음 무관)을 삭제합니다. 삭제 대상은 요청 사용자 본인의 알림으로만 한정되며, 응답 `data.deleted_count` 에 삭제된 건수를 반환합니다. 되돌릴 수 없는 작업이므로 UI 에서 확인 절차를 거친 뒤 호출합니다. + + +### POST /api/admin/notifications/read-all + +- **라우트명**: `api.admin.notifications.read-all` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationController@markAllAsRead` +- **인증/권한**: `auth:sanctum` + `permission:core.notifications.update` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.notifications.update`)이 없는 경우 | + + + +**설명** + +인증된 관리자 본인의 미읽음 알림을 모두 읽음 처리합니다. 처리 대상 `read_at` 을 현재 시각으로 갱신하며, 응답 `data.marked_count` 에 읽음 처리된 건수를 반환합니다. 이미 읽음 상태인 알림은 대상에서 제외됩니다. + + +### POST /api/admin/notifications/read-batch + +- **라우트명**: `api.admin.notifications.read-batch` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationController@markBatchAsRead` +- **인증/권한**: `auth:sanctum` + `permission:core.notifications.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1, max 100 | 대상 리소스 식별자 배열 (대량 작업 대상) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification.filter_batch_read_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.notifications.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +`ids` 배열로 지정한 알림들을 일괄 읽음 처리합니다. 처리 대상은 요청 사용자 본인의 미읽음 알림 중 지정된 ID 에 해당하는 것으로 한정되며, 이미 읽음 상태이거나 본인 소유가 아닌 ID 는 무시됩니다. 응답 `data.marked_count` 에 실제 읽음 처리된 건수를 반환합니다. `ids` 는 최소 1개, 최대 100개까지 전달할 수 있습니다. + + +### GET /api/admin/notifications/unread-count + +- **라우트명**: `api.admin.notifications.unread-count` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationController@unreadCount` +- **인증/권한**: `auth:sanctum` + `permission:core.notifications.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| unread_count | integer | `0` | unread 개수 (집계) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | + + + +**설명** + +인증된 관리자 본인의 미읽음 알림 개수를 집계해 `data.unread_count` 로 반환합니다. `_admin_base.json` 헤더 알림 벨의 미읽음 배지에 사용되며, WebSocket 으로 새 알림이 수신되면 이 값을 재조회해 갱신합니다. + + +### DELETE /api/admin/notifications/{notification} + +- **라우트명**: `api.admin.notifications.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:core.notifications.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| notification | path | string | 예 | — | 대상 notification의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.notifications.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +경로의 `{notification}` ID 에 해당하는 알림 1건을 삭제합니다. 대상은 요청 사용자 본인의 알림으로 한정되며, 해당 ID 가 본인 소유로 존재하지 않으면 404(`notification.user.not_found`)를 반환합니다. + + +### PATCH /api/admin/notifications/{notification}/read + +- **라우트명**: `api.admin.notifications.read` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationController@markAsRead` +- **인증/권한**: `auth:sanctum` + `permission:core.notifications.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| notification | path | string | 예 | — | 대상 notification의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.notifications.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +경로의 `{notification}` ID 에 해당하는 알림 1건을 읽음 처리하고, 갱신된 알림 리소스를 `data` 로 반환합니다. 대상은 요청 사용자(관리자) 본인의 알림으로 한정되며, 본인 소유로 존재하지 않으면 404(`notification.user.not_found`)를 반환합니다. 반환 리소스에는 `id`, `type`, `type_label`, `subject`, `body`, `url`, `read_at`, `created_at` 가 포함됩니다. + + +### GET /api/user/notifications + +- **라우트명**: `api.user.notifications.index` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\NotificationController@index` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.user-notifications.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| read | query | string | 아니오 | `unread`, `read`, `all` | 읽음 상태 필터 (unread: 미읽음(`read_at` null)만, read: 읽음(`read_at` not null)만, all: 전체). 미지정 시 전체 | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification.filter_index_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.user-notifications.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +인증된 사용자 본인의 사이트내 알림 목록을 최신순(`created_at desc`)으로 페이지네이션해 반환합니다. `read` 파라미터로 미읽음(`unread`)·읽음(`read`)·전체(`all`, 기본)를 필터링하고, `per_page` 미지정 시 20건 단위로 반환합니다. 조회 대상은 항상 요청 사용자 본인의 알림으로 한정됩니다. `_user_base.json` 이 이 엔드포인트를 소비하며 WebSocket 알림 수신 시 갱신됩니다. 관리자 스코프(`/api/admin/notifications`)와 동일한 서비스·리소스를 사용하되 권한(`core.user-notifications.*`)과 기본 페이지 크기(20건)가 다릅니다. + + +### DELETE /api/user/notifications/all + +- **라우트명**: `api.user.notifications.destroy-all` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\NotificationController@destroyAll` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.user-notifications.delete` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.user-notifications.delete`)이 없는 경우 | + + + +**설명** + +인증된 사용자 본인의 모든 사이트내 알림(읽음·미읽음 무관)을 삭제합니다. 삭제 대상은 요청 사용자 본인의 알림으로만 한정되며, 응답 `data.deleted_count` 에 삭제된 건수를 반환합니다. 되돌릴 수 없는 작업입니다. + + +### POST /api/user/notifications/read-all + +- **라우트명**: `api.user.notifications.read-all` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\NotificationController@markAllAsRead` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.user-notifications.update` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.user-notifications.update`)이 없는 경우 | + + + +**설명** + +인증된 사용자 본인의 미읽음 알림을 모두 읽음 처리합니다. 처리 대상 `read_at` 을 현재 시각으로 갱신하며, 응답 `data.marked_count` 에 읽음 처리된 건수를 반환합니다. 이미 읽음 상태인 알림은 대상에서 제외됩니다. + + +### POST /api/user/notifications/read-batch + +- **라우트명**: `api.user.notifications.read-batch` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\NotificationController@markBatchAsRead` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.user-notifications.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1, max 100 | 대상 리소스 식별자 배열 (대량 작업 대상) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.notification.filter_batch_read_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.user-notifications.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +`ids` 배열로 지정한 알림들을 일괄 읽음 처리합니다. 처리 대상은 요청 사용자 본인의 미읽음 알림 중 지정된 ID 에 해당하는 것으로 한정되며, 이미 읽음 상태이거나 본인 소유가 아닌 ID 는 무시됩니다. 응답 `data.marked_count` 에 실제 읽음 처리된 건수를 반환합니다. `ids` 는 최소 1개, 최대 100개까지 전달할 수 있습니다. + + +### GET /api/user/notifications/unread-count + +- **라우트명**: `api.user.notifications.unread-count` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\NotificationController@unreadCount` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.user-notifications.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| unread_count | integer | `0` | unread 개수 (집계) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.user-notifications.read`)이 없는 경우 | + + + +**설명** + +인증된 사용자 본인의 미읽음 알림 개수를 집계해 `data.unread_count` 로 반환합니다. `_user_base.json` 의 알림 미읽음 배지에 사용되며, WebSocket 으로 새 알림이 수신되면 이 값을 재조회해 갱신합니다. + + +### DELETE /api/user/notifications/{notification} + +- **라우트명**: `api.user.notifications.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\NotificationController@destroy` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.user-notifications.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| notification | path | string | 예 | — | 대상 notification의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.user-notifications.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +경로의 `{notification}` ID 에 해당하는 알림 1건을 삭제합니다. 대상은 요청 사용자 본인의 알림으로 한정되며, 해당 ID 가 본인 소유로 존재하지 않으면 404(`notification.user.not_found`)를 반환합니다. + + +### PATCH /api/user/notifications/{notification}/read + +- **라우트명**: `api.user.notifications.read` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\NotificationController@markAsRead` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.user-notifications.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| notification | path | string | 예 | — | 대상 notification의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.user-notifications.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +경로의 `{notification}` ID 에 해당하는 알림 1건을 읽음 처리하고, 갱신된 알림 리소스를 `data` 로 반환합니다. 대상은 요청 사용자 본인의 알림으로 한정되며, 본인 소유로 존재하지 않으면 404(`notification.user.not_found`)를 반환합니다. 반환 리소스에는 `id`, `type`, `type_label`, `subject`, `body`, `url`, `read_at`, `created_at` 가 포함됩니다. + + diff --git a/docs/backend/api/password.md b/docs/backend/api/password.md new file mode 100644 index 00000000..5de1eb60 --- /dev/null +++ b/docs/backend/api/password.md @@ -0,0 +1,53 @@ +# Password API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Password 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### PUT /api/me/password + +- **라우트명**: `api.me.password` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@changePassword` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| current_password | body | string | 예 | — | 현재 비밀번호 (변경 전 확인용) | +| password | body | string | 예 | — | 비밀번호 | +| password_confirmation | body | string | 예 | — | 비밀번호 확인 (password 와 일치해야 함) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.change_password_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +현재 로그인한 사용자가 자신의 비밀번호를 변경한다. 프론트의 비밀번호 변경 화면(`mypage/change-password.json`)이 사용한다. `current_password` 는 `current_password:sanctum` 규칙으로 검증되어 현재 비밀번호가 틀리면 실패하며, 새 비밀번호는 8자 이상이면서 `password_confirmation` 과 일치해야 한다. 해싱은 `UserService::updateUser()` 가 담당한다. 비밀번호 변경이 본인인증(IDV) 대상으로 설정된 경우 확장이 428 을 유발할 수 있으며, 이는 글로벌 예외 핸들러가 처리한다. + + diff --git a/docs/backend/api/permissions.md b/docs/backend/api/permissions.md new file mode 100644 index 00000000..36bc7714 --- /dev/null +++ b/docs/backend/api/permissions.md @@ -0,0 +1,52 @@ +# Permissions API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Permissions 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/permissions + +- **라우트명**: `api.admin.permissions.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PermissionController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.permissions.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `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`)를 담는다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.permissions.read`)이 없는 경우 | + + + +**설명** + +시스템의 전체 권한을 계층형 트리로 조회한다. 역할 생성·편집 화면(`admin_role_form.json`)의 권한 선택 트리를 채우는 데 사용된다. 권한은 모듈 → 카테고리 → 개별 권한 순으로 중첩되며(각 노드의 `children`), 코어 권한이 먼저 오도록 정렬된다. 응답의 `permissions` 는 권한 타입별(admin/user)로 그룹화되고, 각 그룹은 `label`·`icon` 메타와 필터링된 권한 트리를 담는다. 함께 반환되는 `types`(권한 타입 목록), `default_type`(기본 탭), `scope_options`(scope_type 선택지: 전체/역할/본인)는 편집 UI 구성에 쓰인다. 리프 노드만 실제 부여 가능한 권한(`is_assignable`)이다. `core.permissions.read` 권한이 필요하다. + + diff --git a/docs/backend/api/plugins.md b/docs/backend/api/plugins.md new file mode 100644 index 00000000..71e414a8 --- /dev/null +++ b/docs/backend/api/plugins.md @@ -0,0 +1,885 @@ +# Plugins API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Plugins 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/plugins + +- **라우트명**: `api.admin.plugins.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| filters | query | array | 아니오 | max 10 | 추가 필터 조건 맵 (필드별 조건) | +| status | query | string | 아니오 | `installed`, `uninstalled`, `active`, `inactive` | 상태 필터 (해당 상태의 항목만 조회) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| include_hidden | query | boolean | 아니오 | — | 숨김 확장 포함 여부 (manifest `hidden=true` 로 목록에서 감춰진 플러그인까지 조회) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.index_validation_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | null | `null` | 기본 키 (내부 식별자) | +| identifier | string | `sirsoft-ckeditor5` | 플러그인 고유 식별자 (vendor-plugin 형식) | +| vendor | string | `sirsoft` | 벤더/개발자명 | +| name | string | `CKEditor 5 WYSIWYG 에디터` | 플러그인 이름 (다국어 JSON) | +| version | string | `1.0.0` | 플러그인 버전 | +| description | string | `CKEditor 5를 이용한 WYSIWYG 에디터 플러그인입니다. …` | 플러그인 설명 (다국어 JSON) | +| dependencies | array | `[]` | 의존하는 확장 맵 (manifest 파생 — {modules, plugins}) | +| permissions | array | `[]` | 연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) | +| roles | array | `[]` | 플러그인이 정의한 역할 목록 (manifest 파생 — 설치 시 시드되는 역할) | +| config | array | `[]` | 플러그인 설정 값 (manifest config 정의 기반 현재 설정 맵) | +| hooks | array | `[]` | 훅 설정 정보 | +| status | string | `active` | 상태 (active: 활성화, inactive: 비활성화, installing: 설치 중, uninstalling: 제거 중, updating: 업데이트 중) | +| is_installed | boolean | `false` | installed 여부 | +| has_settings | boolean | `true` | settings 여부 | +| settings_route | string | `/admin/plugins/sirsoft-ckeditor5/sett…` | 설정 페이지 경로 (설정 UI 진입 라우트, 설정 미제공 시 null) | +| assets | object | `{"js":"\/api\/plugins\/assets\/sirsoft-ckeditor5\/dist\/j…` | 프론트엔드 에셋 매니페스트 (manifest 파생 — js/css 진입점·로딩 전략) | +| update_available | boolean | `false` | 최신 버전 대비 업데이트 가능 여부 | +| update_source | null | `null` | 업데이트 감지 출처 (github, bundled 등) | +| latest_version | string | `1.0.0` | 감지된 최신 배포 버전 | +| file_version | string | `1.0.0` | 설치된 파일의 manifest 버전 | +| github_url | string | `https://github.com/gnuboard/g7-plugin…` | GitHub 저장소 URL | +| github_changelog_url | string | `https://github.com/gnuboard/g7-plugin…` | GitHub 변경 내역 URL | +| is_pending | boolean | `false` | _pending 대기소에 있어 설치 대기 중인지 여부 | +| is_bundled | boolean | `false` | 코어에 선탑재된 번들 확장인지 여부 | +| deactivated_reason | null | `null` | 비활성화 사유: manual(사용자 수동) \| incompatible_core(코어 버전 호환성) \| null(active) | +| deactivated_at | null | `null` | deactivated 일시 | +| incompatible_required_version | null | `null` | 요구 코어 버전 미충족 시 필요한 버전 (호환되면 null) | +| abilities | object | `{"can_install":true,"can_activate":true,"can_uninstall":t…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 설치된 플러그인과 미설치 플러그인을 모두 포함한 전체 플러그인 목록을 페이지네이션으로 조회합니다. `search` 는 이름·식별자·설명·벤더에 대한 OR 검색이고 `filters` 는 AND 조건으로 적용됩니다. `core.plugins.read` 권한이 필요하며, 응답의 `abilities` 는 현재 사용자의 install/activate/uninstall 권한 보유 여부를 담습니다. 관리자 플러그인 관리 화면의 목록 그리드를 구성하는 기본 엔드포인트입니다. + + +### POST /api/admin/plugins/activate + +- **라우트명**: `api.admin.plugins.activate` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@activate` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.activate` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| plugin_name | body | string | 예 | max 255 | plugin 이름 (식별자) | +| force | body | boolean | 아니오 | — | 강제 실행 여부 (안전 확인/선행 검사 우회) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.activate_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.activate`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 설치된 플러그인을 활성화합니다. `core.plugins.activate` 권한이 필요합니다. `force` 없이 호출했을 때 필요한 의존 확장이 충족되지 않으면 409 응답으로 `missing_modules`·`missing_plugins` 목록과 함께 경고를 반환하므로, 사용자 확인 후 `force: true` 로 재요청해야 합니다. 재활성화 시 cascade 로 함께 비활성화됐던 번들 언어팩 목록이 `pending_language_packs` 로 응답에 포함됩니다. + + +### POST /api/admin/plugins/check-updates + +- **라우트명**: `api.admin.plugins.check-updates` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@checkUpdates` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.install` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.install`)이 없는 경우 | + + + +**설명** 설치된 모든 플러그인에 대해 GitHub·번들 소스를 조회하여 새 버전 배포 여부를 일괄 확인합니다. `core.plugins.install` 권한이 필요합니다. 파라미터 없이 호출하며, 각 플러그인의 업데이트 가능 여부와 감지된 최신 버전 정보를 반환합니다. 플러그인 목록 화면 진입 시 업데이트 뱃지를 갱신하는 용도로 사용됩니다. + + +### POST /api/admin/plugins/deactivate + +- **라우트명**: `api.admin.plugins.deactivate` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@deactivate` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.activate` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| plugin_name | body | string | 예 | max 255 | plugin 이름 (식별자) | +| force | body | boolean | 아니오 | — | 강제 실행 여부 (안전 확인/선행 검사 우회) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.deactivate_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.activate`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 활성 플러그인을 비활성화합니다. `core.plugins.activate` 권한이 필요합니다. `force` 없이 호출했을 때 이 플러그인에 의존하는 템플릿·모듈·플러그인이 있으면 409 응답으로 `dependent_templates`·`dependent_modules`·`dependent_plugins` 목록과 함께 경고를 반환합니다. 의존 관계 확인 후 `force: true` 로 강제 비활성화할 수 있습니다. + + +### POST /api/admin/plugins/install + +- **라우트명**: `api.admin.plugins.install` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@install` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| plugin_name | body | string | 예 | max 255 | plugin 이름 (식별자) | +| vendor_mode | body | string | 아니오 | `auto`, `composer`, `bundled` | 벤더 설치 모드 (auto/composer/bundled) | +| dependencies | body | array | 아니오 | — | 함께 설치할 의존 확장 목록 (install-preview 응답 기반 사용자 선택 — 원소 type: module\|plugin, identifier) | +| language_packs | body | array | 아니오 | — | 함께 설치할 번들 언어팩 식별자 목록 (best-effort cascade 2단계) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.install_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** `_pending`·`_bundled` 대기소에 있는 플러그인을 활성 디렉토리로 설치합니다. `core.plugins.install` 권한이 필요합니다. `vendor_mode` 로 Composer 의존성 설치 방식을(auto/composer/bundled) 지정하며, 요청 본문의 `dependencies` 로 선택한 의존 확장을 먼저 설치(cascade 1단계, 실패 시 전체 중단)한 뒤 `language_packs` 로 지정한 번들 언어팩을 best-effort 로 함께 설치합니다(cascade 2단계). 언어팩 설치 실패는 응답의 `language_pack_failures` 에 담겨 반환됩니다. + + +### POST /api/admin/plugins/install-from-file + +- **라우트명**: `api.admin.plugins.install-from-file` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@installFromFile` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| file | body | file | 예 | max 51200 | 업로드 파일 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.install_from_file_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 업로드된 ZIP 파일에서 플러그인을 설치합니다. `core.plugins.install` 권한이 필요하며, 파일은 최대 50MB(51200KB)까지 허용됩니다. ZIP 압축 해제 후 plugin.json 검증을 거쳐 설치하며, 성공 시 201 상태로 설치된 플러그인 정보를 반환합니다. 설치 전 manifest 만 미리 확인하려면 `manifest-preview` 를 먼저 호출하는 것이 안전합니다. + + +### POST /api/admin/plugins/install-from-github + +- **라우트명**: `api.admin.plugins.install-from-github` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@installFromGithub` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| github_url | body | string | 예 | — | GitHub 저장소 URL | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.install_from_github_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** GitHub 저장소 URL 에서 플러그인을 내려받아 설치합니다. `core.plugins.install` 권한이 필요합니다. `github_url` 로 지정한 공개 저장소의 릴리스/소스를 받아 압축 해제·검증 후 설치하며, 성공 시 201 상태로 설치된 플러그인 정보를 반환합니다. + + +### GET /api/admin/plugins/installed + +- **라우트명**: `api.admin.plugins.installed` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@installed` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | null | `null` | 기본 키 (내부 식별자) | +| identifier | string | `sirsoft-ckeditor5` | 플러그인 고유 식별자 (vendor-plugin 형식) | +| vendor | string | `sirsoft` | 벤더/개발자명 | +| name | string | `CKEditor 5 WYSIWYG 에디터` | 플러그인 이름 (다국어 JSON) | +| version | string | `1.0.0` | 플러그인 버전 | +| description | string | `CKEditor 5를 이용한 WYSIWYG 에디터 플러그인입니다. …` | 플러그인 설명 (다국어 JSON) | +| dependencies | array | `[]` | 의존하는 확장 맵 (manifest 파생 — {modules, plugins}) | +| permissions | array | `[]` | 연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) | +| roles | array | `[]` | 플러그인이 정의한 역할 목록 (manifest 파생 — 설치 시 시드되는 역할) | +| config | array | `[]` | 플러그인 설정 값 (manifest config 정의 기반 현재 설정 맵) | +| hooks | array | `[]` | 훅 설정 정보 | +| status | string | `active` | 상태 (active: 활성화, inactive: 비활성화, installing: 설치 중, uninstalling: 제거 중, updating: 업데이트 중) | +| is_installed | boolean | `false` | installed 여부 | +| has_settings | boolean | `true` | settings 여부 | +| settings_route | string | `/admin/plugins/sirsoft-ckeditor5/sett…` | 설정 페이지 경로 (설정 UI 진입 라우트, 설정 미제공 시 null) | +| assets | object | `{"js":"\/api\/plugins\/assets\/sirsoft-ckeditor5\/dist\/j…` | 프론트엔드 에셋 매니페스트 (manifest 파생 — js/css 진입점·로딩 전략) | +| update_available | boolean | `false` | 최신 버전 대비 업데이트 가능 여부 | +| update_source | null | `null` | 업데이트 감지 출처 (github, bundled 등) | +| latest_version | string | `1.0.0` | 감지된 최신 배포 버전 | +| file_version | string | `1.0.0` | 설치된 파일의 manifest 버전 | +| github_url | string | `https://github.com/gnuboard/g7-plugin…` | GitHub 저장소 URL | +| github_changelog_url | string | `https://github.com/gnuboard/g7-plugin…` | GitHub 변경 내역 URL | +| is_pending | boolean | `false` | _pending 대기소에 있어 설치 대기 중인지 여부 | +| is_bundled | boolean | `false` | 코어에 선탑재된 번들 확장인지 여부 | +| deactivated_reason | null | `null` | 비활성화 사유: manual(사용자 수동) \| incompatible_core(코어 버전 호환성) \| null(active) | +| deactivated_at | null | `null` | deactivated 일시 | +| incompatible_required_version | null | `null` | 요구 코어 버전 미충족 시 필요한 버전 (호환되면 null) | +| abilities | object | `{"can_install":true,"can_activate":true,"can_uninstall":t…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 현재 설치된 플러그인만 조회합니다(미설치 항목 제외). 이 엔드포인트는 세부 권한 미들웨어 없이 `auth:sanctum` 인증만 요구하므로, 다른 화면이 활성/설치된 플러그인 목록을 참조할 때 사용하는 경량 조회 API 입니다. 페이지네이션 없이 설치된 항목 배열을 반환합니다. + + +### POST /api/admin/plugins/manifest-preview + +- **라우트명**: `api.admin.plugins.manifest-preview` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@manifestPreview` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| file | body | file | 예 | max 51200 | 업로드 파일 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 업로드된 ZIP 파일의 plugin.json manifest 와 검증 결과만 추출합니다(실제 설치는 수행하지 않음). `core.plugins.install` 권한이 필요하며 파일은 최대 50MB 까지 허용됩니다. 설치 모달에서 사용자가 파일 선택 직후 manifest 유효성과 검증 실패 사유를 미리 확인하는 용도입니다. 검증 오류 시 422 로 사유를 반환합니다. + + +### POST /api/admin/plugins/refresh-layouts + +- **라우트명**: `api.admin.plugins.refresh-layouts` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@refreshLayouts` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.activate` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| plugin_name | body | string | 예 | max 255 | plugin 이름 (식별자) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.refresh_layouts_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.activate`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 플러그인의 레이아웃 파일을 다시 읽어 DB 에 동기화합니다. `core.plugins.activate` 권한이 필요합니다. 파일에서 변경된 레이아웃은 갱신되고 삭제된 레이아웃은 DB 에서도 제거되며, 응답으로 created/updated/deleted/unchanged 건수를 반환합니다. 플러그인의 `_bundled` 레이아웃 JSON 을 수정한 뒤 재빌드 없이 반영할 때 사용합니다. + + +### DELETE /api/admin/plugins/uninstall + +- **라우트명**: `api.admin.plugins.uninstall` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@uninstall` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.uninstall` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| plugin_name | query | string | 예 | max 255 | plugin 이름 (식별자) | +| delete_data | query | boolean | 아니오 | — | 데이터 삭제 여부 (true 시 플러그인이 생성한 DB 데이터까지 함께 삭제, 미지정 시 데이터 보존) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.uninstall_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.uninstall`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 플러그인을 시스템에서 제거합니다. `core.plugins.uninstall` 권한이 필요합니다. 활성 디렉토리만 삭제하고 `_bundled` 원본은 보존합니다. `delete_data: true` 인 경우 플러그인이 생성한 DB 데이터까지 함께 삭제하며, 기본값은 데이터 보존입니다. 삭제될 데이터 범위는 사전에 `uninstall-info` 로 확인할 수 있습니다. + + +### GET /api/admin/plugins/{identifier}/changelog + +- **라우트명**: `api.admin.plugins.changelog` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@changelog` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| source | query | string | 아니오 | `active`, `bundled`, `github` | 변경 내역 조회 출처 (active: 활성 설치본, bundled: 번들 원본, github: 원격 저장소) | +| from_version | query | string | 아니오 | — | 시작 버전 (범위 하한) | +| to_version | query | string | 아니오 | — | 대상 버전 (범위 상한) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.extension.changelog_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 플러그인의 변경 내역(CHANGELOG)을 조회합니다. `core.plugins.read` 권한이 필요합니다. `source` 로 조회 출처를(active: 활성 설치본, bundled: 번들 원본, github: 원격 저장소) 선택하고, `from_version`·`to_version` 으로 버전 구간을 좁힐 수 있습니다. 업데이트 전 사용자에게 변경 사항을 안내하는 데 사용됩니다. + + +### GET /api/admin/plugins/{identifier}/dependent-templates + +- **라우트명**: `api.admin.plugins.dependent-templates` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@dependentTemplates` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 이 플러그인에 의존하는 템플릿 목록을 조회합니다. `core.plugins.read` 권한이 필요합니다. 응답으로 의존 템플릿 배열과 총 개수를 반환하며, 플러그인 비활성화·제거 전 영향을 받는 템플릿을 사용자에게 미리 알리는 데 사용됩니다. + + +### GET /api/admin/plugins/{identifier}/license + +- **라우트명**: `api.admin.plugins.license` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@license` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 플러그인에 포함된 라이선스 파일의 원문 내용을 반환합니다. `core.plugins.read` 권한이 필요합니다. `identifier` 는 소문자·숫자·하이픈·언더스코어 형식만 허용되며 형식에 맞지 않거나 라이선스 파일이 없으면 404 를 반환합니다. 라이선스 고지 화면에 전문을 표시하는 용도입니다. + + +### GET /api/admin/plugins/{identifier}/settings + +- **라우트명**: `api.admin.plugins.settings.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginSettingsController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 플러그인의 현재 설정 값을 조회합니다. `core.plugins.read` 권한이 필요합니다. 저장된 설정이 없거나 플러그인을 찾을 수 없으면 404 를 반환합니다. 설정 페이지 진입 시 폼의 현재 값을 채우는 용도이며, 폼 스키마/UI 구성은 별도의 `settings/layout` 엔드포인트에서 조회합니다. + + +### PUT /api/admin/plugins/{identifier}/settings + +- **라우트명**: `api.admin.plugins.settings.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginSettingsController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin_settings.update_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 플러그인의 설정 값을 저장합니다. `core.plugins.update` 권한이 필요합니다. 검증된 값을 우선 사용하되, PluginManager 에 등록되지 않아 검증 규칙이 없는 플러그인의 경우 요청 본문 전체(`all()`)를 저장합니다. 저장 실패 시 500 을 반환하고, 성공 시 갱신된 설정 값을 함께 반환합니다. + + +### GET /api/admin/plugins/{identifier}/settings/layout + +- **라우트명**: `api.admin.plugins.settings.layout` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginSettingsController@layout` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 플러그인 설정 페이지의 UI 구성과 설정 스키마(레이아웃)를 조회합니다. `core.plugins.read` 권한이 필요합니다. 레이아웃이 정의되지 않았거나 플러그인을 찾을 수 없으면 404 를 반환합니다. 설정 값 조회(`settings`)와 짝을 이루어 설정 화면을 렌더링하는 데 사용됩니다. + + +### GET /api/admin/plugins/{pluginName} + +- **라우트명**: `api.admin.plugins.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| pluginName | path | string | 예 | — | 대상 plugin의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 플러그인의 상세 정보를 조회합니다. `core.plugins.read` 권한이 필요합니다. 목록보다 자세한 `toDetailArray()` 형태를 반환하며, 이 플러그인이 지원하는 번들 언어팩 정보가 함께 주입됩니다. 플러그인을 찾을 수 없으면 404 를 반환합니다. + + +### GET /api/admin/plugins/{pluginName}/check-modified-layouts + +- **라우트명**: `api.admin.plugins.check-modified-layouts` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@checkModifiedLayouts` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| pluginName | path | string | 예 | — | 대상 plugin의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 플러그인에서 사용자가 수정한 레이아웃이 있는지 확인합니다. `core.plugins.read` 권한이 필요합니다. 업데이트 실행 전 이 정보를 조회하여 레이아웃 전략(overwrite: 새 버전으로 교체, keep: 사용자 수정본 유지) 선택을 안내하는 데 사용됩니다. + + +### GET /api/admin/plugins/{pluginName}/install-preview + +- **라우트명**: `api.admin.plugins.install-preview` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@installPreview` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| pluginName | path | string | 예 | — | 대상 plugin의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.install`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 플러그인 설치 시 함께 처리될 cascade 후보(의존 확장 + 동반 가능한 번들 언어팩) 트리를 반환합니다. `core.plugins.install` 권한이 필요합니다. 설치 모달 오픈 시 호출되어 사용자가 함께 설치할 항목을 선택하도록 노출하며, ZIP 업로드 기반의 `manifest-preview` 와 달리 이미 알려진 식별자에 대한 GET 조회입니다. + + +### GET /api/admin/plugins/{pluginName}/uninstall-info + +- **라우트명**: `api.admin.plugins.uninstall-info` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@uninstallInfo` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.uninstall` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| pluginName | path | string | 예 | — | 대상 plugin의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.uninstall`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 플러그인 제거 시 삭제될 데이터 정보를 조회합니다. `core.plugins.uninstall` 권한이 필요합니다. 제거 확인 모달에서 사용자에게 어떤 데이터가 사라지는지 미리 보여주는 용도이며, 플러그인을 찾을 수 없으면 404 를 반환합니다. + + +### POST /api/admin/plugins/{pluginName}/update + +- **라우트명**: `api.admin.plugins.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\PluginController@performUpdate` +- **인증/권한**: `auth:sanctum` + `permission:core.plugins.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| pluginName | path | string | 예 | — | 대상 plugin의 이름 (식별자) | +| layout_strategy | body | string | 아니오 | `overwrite`, `keep` | 레이아웃 처리 전략 (overwrite: 새 버전으로 교체, keep: 사용자 수정본 유지) | +| vendor_mode | body | string | 아니오 | `auto`, `composer`, `bundled` | 벤더 설치 모드 (auto/composer/bundled) | +| force | body | boolean | 아니오 | — | 강제 실행 여부 (안전 확인/선행 검사 우회) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin.perform_update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.plugins.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 플러그인을 최신 버전으로 업데이트합니다. `core.plugins.install` 권한이 필요합니다. `layout_strategy` 로 레이아웃 처리 방식을(overwrite: 새 버전으로 교체, keep: 사용자 수정본 유지) 지정하며, `vendor_mode` 로 Composer 의존성 처리 방식을 선택합니다. 버전 제약·호환성 문제로 막힐 경우 `force: true` 로 강제 진행할 수 있습니다. + + +### GET /api/plugins/assets/{identifier}/{path} + +- **라우트명**: `api.public.plugins.assets` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicPluginController@serveAsset` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| path | path | string | 예 | — | 경로 | +| identifier | query | string | 예 | — | 대상 확장/리소스의 식별자 | +| path | query | string | 예 | — | 경로 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 플러그인의 개별 프론트엔드 에셋 파일(JS/CSS/이미지 등)을 서빙하는 공개 엔드포인트입니다. 인증이 필요하지 않으며, 경로·확장자 보안 검증은 FormRequest 에서 완료됩니다. 플러그인 미존재·파일 미존재·허용되지 않은 파일 유형은 각각 404/404/403 으로 응답하고, 정상 파일은 ETag 와 1년 캐시 헤더를 붙여 반환합니다. 소스맵 등 개별 에셋을 직접 참조할 때 사용되며, 통합 로딩은 `bundle.js`/`bundle.css` 를 사용합니다. + + +### GET /api/plugins/bundle.css + +- **라우트명**: `api.public.plugins.bundle.css` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicPluginController@serveBundleCss` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +_대표 에러 없음 (공개 조회 — 활성 에셋이 없으면 빈 200 응답)._ + + + +**설명** 활성 플러그인들의 프론트엔드 CSS 를 서버에서 하나로 병합한 번들을 서빙하는 공개 엔드포인트입니다. 인증이 필요하지 않습니다. 활성 global 플러그인 에셋이 없으면 빈 200(text/css) 응답을 반환하고, 있으면 병합 파일을 ETag·환경별 Cache-Control 과 함께 서빙합니다. 페이지가 플러그인 스타일을 요청 1건으로 로드하도록 합니다. + + +### GET /api/plugins/bundle.js + +- **라우트명**: `api.public.plugins.bundle.js` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicPluginController@serveBundleJs` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +_대표 에러 없음 (공개 조회 — 활성 에셋이 없으면 빈 200 응답)._ + + + +**설명** 활성 플러그인들의 프론트엔드 IIFE JS 를 서버에서 하나로 병합한 번들을 서빙하는 공개 엔드포인트입니다. 인증이 필요하지 않습니다. 활성 global 플러그인 에셋이 없으면 빈 200(text/javascript) 응답을 반환하고, 있으면 병합 파일을 ETag·환경별 Cache-Control 과 함께 서빙합니다. 프론트는 `G7Config.bundleUrls` 를 읽어 이 번들을 로드합니다. + + +### GET /api/plugins/{identifier}/components.json + +- **라우트명**: `api.public.plugins.components` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicPluginController@serveComponents` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 플러그인의 컴포넌트 정의 파일(components.json)을 서빙하는 공개 엔드포인트입니다. 인증이 필요하지 않습니다. 편집 모드 부팅 시 ComponentRegistry 가 활성 확장 매니페스트를 네임스페이스 병합하기 위해 fetch 하며, 구버전 플러그인처럼 파일이 없으면 빈 components 로 폴백합니다. 응답은 1시간 캐시됩니다. 플러그인 미존재 시 404. + + +### GET /api/plugins/{identifier}/editor-spec + +- **라우트명**: `api.public.plugins.editor_spec` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicPluginController@serveEditorSpec` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 플러그인의 레이아웃 편집기 스펙(editor-spec.json)을 서빙하는 공개 엔드포인트입니다. 인증이 필요하지 않습니다. 활성 플러그인만 대상으로 하며 활성 디렉토리 → `_bundled` 폴백 순으로 읽어 `data.spec` 형태로 반환합니다. 비활성·미존재 플러그인은 404 이고, 편집기 스펙 파일을 작성하지 않은 경우 spec=null 로 정상 응답합니다. + + diff --git a/docs/backend/api/profile.md b/docs/backend/api/profile.md new file mode 100644 index 00000000..caea6ea1 --- /dev/null +++ b/docs/backend/api/profile.md @@ -0,0 +1,155 @@ +# Profile API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Profile 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/user/profile + +- **라우트명**: `api.user.profile.show` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@show` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.profile.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.profile.read`)이 없는 경우 | + + + +**설명** + +`GET /api/me` 와 동일하게 `ProfileController@show` 를 호출해 현재 사용자의 프로필을 조회하지만, `permission:core.profile.read` 권한 미들웨어가 추가된 경로다. 응답 형태는 `UserResource::toProfileArray()` 산물로 `GET /api/me` 와 같으며, 필드별 소유(확장 병합) 규칙도 동일하다. 실측 예시가 비어 있는 것은 문서 생성 시 샘플 사용자가 해당 권한을 갖지 못해 403 이 반환되었기 때문이며, 응답 필드는 `GET /api/me` 문서를 참조한다. + + +### PUT /api/user/profile + +- **라우트명**: `api.user.profile.update` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@update` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.profile.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | string | 예 | max 255 | 대상의 이름/명칭 | +| nickname | body | string | 아니오 | max 50 | 닉네임 | +| email | body | email | 예 | max 255 | 이메일 주소 | +| password | body | string | 아니오 | — | 비밀번호 | +| current_password | body | string | 아니오 | — | 현재 비밀번호 (변경 전 확인용) | +| language | body | string | 아니오 | — | 언어 코드 | +| country | body | string | 아니오 | — | 국가 코드 (ISO 3166-1 alpha-2) | +| timezone | body | string | 아니오 | — | 타임존 식별자 | +| homepage | body | string | 아니오 | max 255 | 홈페이지 URL | +| mobile | body | string | 아니오 | max 20 | 휴대전화 번호 | +| phone | body | string | 아니오 | max 20 | 전화번호 | +| zipcode | body | string | 아니오 | max 10 | 우편번호 | +| address | body | string | 아니오 | max 255 | 기본 주소 | +| address_detail | body | string | 아니오 | max 255 | 상세 주소 | +| signature | body | string | 아니오 | max 1000 | 서명 | +| bio | body | string | 아니오 | max 5000 | 자기소개 | +| notify_post_complete | body | boolean | 아니오 | — | 내 글에 답변/처리 완료 시 알림 수신 여부 (게시판 모듈 알림 설정) | +| notify_post_reply | body | boolean | 아니오 | — | 내 글에 답글이 달렸을 때 알림 수신 여부 (게시판 모듈 알림 설정) | +| notify_comment | body | boolean | 아니오 | — | 내 글에 댓글이 달렸을 때 알림 수신 여부 (게시판 모듈 알림 설정) | +| notify_reply_comment | body | boolean | 아니오 | — | 내 댓글에 대댓글이 달렸을 때 알림 수신 여부 (게시판 모듈 알림 설정) | +| email_subscription | body | boolean | 아니오 | — | 광고성 이메일 수신 동의 여부 (sirsoft-marketing 채널, 훅 주입 파라미터) | +| marketing_consent | body | boolean | 아니오 | — | 마케팅 정보 수신 전체 동의 마스터 키 — 마케팅 채널 전체 동의/철회 제어 (sirsoft-marketing 훅 주입 파라미터) | +| third_party_consent | body | boolean | 아니오 | — | 제3자 정보 제공 동의 여부 (법적 필수 항목, sirsoft-marketing 훅 주입 파라미터) | +| info_disclosure | body | boolean | 아니오 | — | 개인정보 이용 안내 동의 여부 (법적 필수 항목, sirsoft-marketing 훅 주입 파라미터) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.update_profile_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.profile.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +`PUT /api/me` 와 동일하게 `ProfileController@update` 를 호출해 프로필을 수정하되, `permission:core.profile.update` 권한 미들웨어가 추가된 경로다. 요청 파라미터와 검증 규칙(`UpdateProfileRequest`), 확장 소유 파라미터 병합(`core.user.update_profile_validation_rules`)은 `PUT /api/me` 와 동일하다. 성공 시 갱신된 프로필이 `UserResource` 형태로 반환된다. + + +### GET /api/user/profile/activity-log + +- **라우트명**: `api.user.profile.activity-log` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@activityLog` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.profile.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`core.profile.read`)이 없는 경우 | + + + +**설명** + +현재 사용자의 최근 활동 로그를 조회한다(`permission:core.profile.read` 필요). `data.activities` 에 최대 50건의 로그가 최신순으로 담기며, 각 항목은 `id`·`action`·`action_label`·`description`(로케일 반영)·`ip_address`·`created_at`(ISO 8601) 필드를 가진다. 실측 예시가 비어 있는 것은 문서 생성 시 샘플 사용자가 권한을 갖지 못해 403 이 반환되었기 때문이다. + + +### POST /api/user/profile/update-language + +- **라우트명**: `api.user.profile.update-language` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@updateLanguage` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 400 | Bad Request | `language` 값이 `config('app.supported_locales')`(기본 `['ko','en']`)에 포함되지 않는 미지원 로케일인 경우 | + + + +**설명** + +현재 사용자의 언어 설정만 변경한다. 요청 본문의 `language` 값을 받아 `config('app.supported_locales')`(기본 `['ko','en']`)에 포함되는지 검사하며, 허용되지 않는 값이면 400 을 반환한다. 프로필 전체 수정 없이 언어만 즉시 전환할 때 사용하며, 성공 시 갱신된 사용자 정보가 `UserResource` 형태로 반환된다. `language` 는 FormRequest 가 아닌 컨트롤러에서 직접 읽어 검증하므로 문서 상단 파라미터 표에는 자동 수집되지 않는다. + + diff --git a/docs/backend/api/roles.md b/docs/backend/api/roles.md new file mode 100644 index 00000000..420fee88 --- /dev/null +++ b/docs/backend/api/roles.md @@ -0,0 +1,307 @@ +# Roles API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Roles 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/roles + +- **라우트명**: `api.admin.roles.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\RoleController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.permissions.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| is_active | query | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `23` | 기본 키 (내부 식별자) | +| identifier | string | `sirsoft-board.archive.manager` | 역할명 (예: admin, user, manager) | +| name | string | `아카이브 게시판 관리자` | 역할 이름 (다국어 JSON) | +| name_raw | object | `{"ko":"아카이브 게시판 관리자","en":"Archive Board Manager"}` | `name` 의 원본 값 (현재 로케일 미해석 원본 JSON/문자열) | +| description | string | `아카이브 게시판의 관리자 역할` | 역할 설명 (다국어 JSON) | +| description_raw | object | `{"ko":"아카이브 게시판의 관리자 역할","en":"Manager role for Archive b…` | `description` 의 원본 값 (현재 로케일 미해석 원본 JSON/문자열) | +| extension_type | string | `module` | 확장 소유 타입: core(코어), module(모듈), plugin(플러그인), NULL(사용자 정의) | +| extension_identifier | string | `sirsoft-board` | 확장 식별자 (예: core, sirsoft-board, sirsoft-payment) | +| extension_name | string | `게시판` | 이 리소스를 소유한 확장의 표시 이름 (manifest name) | +| is_deletable | boolean | `false` | deletable 여부 | +| is_active | boolean | `true` | active 여부 | +| users_count | integer | `1` | users 개수 (집계) | +| permission_ids | array | `[312,313,314,315,316,317,318,319,320,321,322,323,324,325,…` | permission 식별자 배열 (연관 리소스 참조) | +| permission_values | array | `[{"id":312,"scope_type":null},{"id":313,"scope_type":null…` | 할당된 각 권한의 id와 적용 범위만 담은 경량 목록 (원소 id/scope_type — 역할-권한 pivot 파생). scope_type: null=전체, role=역할 범위, self=본인 범위 | +| permissions | array | `[{"id":85,"parent_id":null,"identifier":"sirsoft-board","…` | 연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) | +| created_at | string | `2026-06-04 09:35:35` | 생성 일시 | +| 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 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.permissions.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +역할 관리 화면(`admin_role_list.json`)의 목록 데이터를 제공하는 페이지네이션 조회 엔드포인트다. `search`(identifier/name 텍스트 검색)와 `is_active`(활성 여부)로 필터링하며 `per_page` 로 페이지 크기를 조절한다. 응답에는 각 역할의 할당 권한(permission_ids/permission_values/permissions), 소유 확장 정보, 사용자 수, 현재 사용자의 조작 가능 여부(abilities)가 포함된다. `core.permissions.read` 권한이 필요하다. + + +### POST /api/admin/roles + +- **라우트명**: `api.admin.roles.store` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\RoleController@store` +- **인증/권한**: `auth:sanctum` + `permission:core.permissions.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | body | string | 예 | max 100 | 대상 확장/리소스의 식별자 | +| name | body | string | 예 | — | 대상의 이름/명칭 | +| description | body | string | 아니오 | — | 설명 | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| permissions | body | array | 아니오 | — | 역할에 부여할 권한 목록. 각 원소는 `{id, scope_type}` (id=권한 식별자, scope_type=적용 범위: null 전체 / role 역할 범위 / self 본인 범위). 전달된 목록 기준으로 역할의 권한 집합이 재설정됨 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.role.store_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.permissions.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +새 역할을 생성한다. `identifier` 는 소문자로 시작하는 영숫자·언더스코어 형식(`^[a-z][a-z0-9_]*$`)이어야 하고 전역 고유해야 한다. `name`·`description` 은 다국어 필드로, 문자열로 보내면 설정된 로케일 전체에 동일 값이 채워지고 객체(`{"ko":..., "en":...}`)로도 보낼 수 있다. `permissions` 는 `[{id, scope_type}]` 형식으로 부여할 권한과 각 권한의 적용 범위(scope_type: null=전체, role, self)를 지정한다. 검증 규칙은 `core.role.store_validation_rules` 필터 훅으로 확장이 확장할 수 있다. `core.permissions.create` 권한이 필요하며 성공 시 201 로 생성된 역할을 반환한다. + + +### GET /api/admin/roles/active + +- **라우트명**: `api.admin.roles.active` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\RoleController@active` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| identifier | string | `admin` | 역할명 (예: admin, user, manager) | +| name | string | `관리자` | 역할 이름 (다국어 JSON) | +| name_raw | object | `{"ko":"관리자","en":"Administrator"}` | `name` 의 원본 값 (현재 로케일 미해석 원본 JSON/문자열) | +| description | string | `시스템의 모든 기능에 접근할 수 있는 최고 관리자입니다.` | 역할 설명 (다국어 JSON) | +| description_raw | object | `{"ko":"시스템의 모든 기능에 접근할 수 있는 최고 관리자입니다.","en":"Super admin…` | `description` 의 원본 값 (현재 로케일 미해석 원본 JSON/문자열) | +| extension_type | string | `core` | 확장 소유 타입: core(코어), module(모듈), plugin(플러그인), NULL(사용자 정의) | +| extension_identifier | string | `core` | 확장 식별자 (예: core, sirsoft-board, sirsoft-payment) | +| extension_name | string | `이커머스` | 이 리소스를 소유한 확장의 표시 이름 (manifest name) | +| is_deletable | boolean | `false` | deletable 여부 | +| is_active | boolean | `true` | active 여부 | +| created_at | string | `2026-05-27 15:20:18` | 생성 일시 | +| 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 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** + +셀렉트 UI(사용자 폼·메뉴 편집의 역할 선택 등)에 채울 활성 역할 목록을 제공한다. 별도 권한 미들웨어가 없어 인증만 되면 호출 가능하지만, 내부에서 권한에 따라 범위가 갈린다. `core.permissions.read` 권한 보유자는 전체 활성 역할을 받고(사용자에게 역할을 부여하는 관리 용도), 미보유자는 자신에게 부여된 활성 역할만 받는다(자기 정보 폼 표시 용도). 응답의 `abilities.can_assign_roles` 는 `core.permissions.update` 권한 보유 여부를 나타낸다. + + +### DELETE /api/admin/roles/{role} + +- **라우트명**: `api.admin.roles.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\RoleController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:core.permissions.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| role | path | string | 예 | — | 대상 role의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.permissions.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +역할을 삭제한다. 코어 소유 역할(admin/user 등)은 403(`role.system_role_delete_error`)으로, 모듈·플러그인이 소유한 확장 역할은 403(`role.extension_owned_role_delete_error`)으로 거부된다. 삭제 가능한(사용자 정의) 역할만 제거되며, CASCADE 에 의존하지 않고 권한·메뉴·사용자 매핑을 명시적으로 해제한 뒤 역할을 삭제한다. `core.permissions.delete` 권한이 필요하다. + + +### GET /api/admin/roles/{role} + +- **라우트명**: `api.admin.roles.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\RoleController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.permissions.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| role | path | string | 예 | — | 대상 role의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `66` | 기본 키 (내부 식별자) | +| identifier | string | `apidoc-sample-role` | 역할명 (예: admin, user, manager) | +| name | string | `API 문서 샘플 역할` | 역할 이름 (다국어 JSON) | +| name_raw | object | `{"ko":"API 문서 샘플 역할","en":"API Doc Sample Role"}` | `name` 의 원본 값 (현재 로케일 미해석 원본 JSON/문자열) | +| description | string | `문서 실측용 역할` | 역할 설명 (다국어 JSON) | +| description_raw | object | `{"ko":"문서 실측용 역할","en":"Sample role for API docs"}` | `description` 의 원본 값 (현재 로케일 미해석 원본 JSON/문자열) | +| extension_type | null | `null` | 확장 소유 타입: core(코어), module(모듈), plugin(플러그인), NULL(사용자 정의) | +| extension_identifier | null | `null` | 확장 식별자 (예: core, sirsoft-board, sirsoft-payment) | +| extension_name | null | `null` | 이 리소스를 소유한 확장의 표시 이름 (manifest name) | +| is_deletable | boolean | `true` | deletable 여부 | +| is_active | boolean | `true` | active 여부 | +| users_count | integer | `0` | users 개수 (집계) | +| permission_ids | array | `[1,85,100]` | permission 식별자 배열 (연관 리소스 참조) | +| permission_values | array | `[{"id":1,"scope_type":null},{"id":85,"scope_type":null},{…` | 할당된 각 권한의 id와 적용 범위만 담은 경량 목록 (원소 id/scope_type — 역할-권한 pivot 파생). scope_type: null=전체, role=역할 범위, self=본인 범위 | +| permissions | array | `[{"id":1,"parent_id":null,"identifier":"core","name":"코어"…` | 연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) | +| created_at | string | `2026-07-06 19:15:16` | 생성 일시 | +| 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 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.permissions.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +단일 역할의 상세 정보를 조회한다. 역할 편집 화면과 복제(clone_from) 시 원본 값을 채우는 데 사용된다. 목록 응답과 달리 permissions 관계를 pivot(scope_type)과 함께 로드하므로 `permission_ids`·`permission_values`·`permissions`(계층 트리)와 `users_count` 가 항상 포함된다. `core.permissions.read` 권한이 필요하다. + + +### PUT /api/admin/roles/{role} + +- **라우트명**: `api.admin.roles.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\RoleController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.permissions.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| role | path | string | 예 | — | 대상 role의 식별자 | +| name | body | string | 예 | — | 대상의 이름/명칭 | +| description | body | string | 아니오 | — | 설명 | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| permissions | body | array | 아니오 | — | 역할에 부여할 권한 목록. 각 원소는 `{id, scope_type}` (id=권한 식별자, scope_type=적용 범위: null 전체 / role 역할 범위 / self 본인 범위). 전달된 목록 기준으로 역할의 권한 집합이 재설정됨 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.role.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.permissions.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +기존 역할의 `name`·`description`·`is_active`·`permissions` 를 수정한다. 생성과 달리 `identifier` 는 변경 대상이 아니며, 각 필드는 `sometimes` 규칙이라 전달된 항목만 갱신된다. `permissions` 를 보내면 `[{id, scope_type}]` 형식으로 역할의 권한 집합 전체가 동기화된다(전달된 목록 기준으로 재설정). `name`·`description` 은 문자열/다국어 객체 양쪽을 받는다. 검증 규칙은 `core.role.update_validation_rules` 필터 훅으로 확장할 수 있다. `core.permissions.update` 권한이 필요하다. + + +### PATCH /api/admin/roles/{role}/toggle-status + +- **라우트명**: `api.admin.roles.toggle-status` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\RoleController@toggleStatus` +- **인증/권한**: `auth:sanctum` + `permission:core.permissions.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| role | path | string | 예 | — | 대상 role의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.permissions.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +역할의 `is_active` 상태를 반대로 토글한다(활성↔비활성). 목록 화면의 상태 스위치에서 호출되며, 별도 본문 없이 대상 역할만 지정하면 된다. 성공 시 사용자 수를 다시 집계한 갱신된 역할 리소스를 반환한다. `core.permissions.update` 권한이 필요하다. + + diff --git a/docs/backend/api/schedules.md b/docs/backend/api/schedules.md new file mode 100644 index 00000000..b4982b9f --- /dev/null +++ b/docs/backend/api/schedules.md @@ -0,0 +1,485 @@ +# Schedules API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Schedules 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/schedules + +- **라우트명**: `api.admin.schedules.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ScheduleController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.schedules.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| filters | query | array | 아니오 | max 10 | 추가 필터 조건 맵 (필드별 조건) | +| type | query | string | 아니오 | `artisan`, `shell`, `url` | 유형 필터 (해당 유형의 항목만 조회) | +| frequency | query | string | 아니오 | `everyMinute`, `hourly`, `daily`, `weekly`, `monthly`, `custom` | 실행 주기 | +| status | query | string | 아니오 | `active`, `inactive` | 상태 필터 (해당 상태의 항목만 조회) | +| last_result | query | string | 아니오 | `success`, `failed`, `running`, `never` | 마지막 실행 결과 필터: success(성공), failed(실패), running(실행중), never(미실행) 중 해당 결과인 스케줄만 조회 | +| without_overlapping | query | string | 아니오 | `0`, `1` | 중복 실행 방지 여부 | +| run_in_maintenance | query | string | 아니오 | `0`, `1` | 점검 모드 중 실행 여부 | +| extension_type | query | string | 아니오 | `core`, `module`, `plugin` | 확장 유형 (core/module/plugin/template) | +| extension_identifier | query | string | 아니오 | max 255 | 확장 식별자 | +| created_from | query | date | 아니오 | — | 생성일 범위의 시작일 (이 날짜 이후 생성된 스케줄만 조회) | +| created_to | query | date | 아니오 | — | 생성일 범위의 종료일 (이 날짜 이전 생성된 스케줄만 조회, created_from 이후여야 함) | +| sort_by | query | string | 아니오 | `created_at`, `name`, `next_run_at`, `last_run_at`, `is_active`, `last_result` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.schedule.list_validation_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | string | `API 문서 샘플 스케줄` | 작업명 | +| 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(실행) | +| 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-05T19:20:23+09:00` | last run 일시 | +| last_duration | null | `null` | 마지막 실행의 소요 시간을 사람이 읽는 문자열로 포맷한 값 (예: "45초", "2분 3초" — 마지막 실행 이력의 duration 파생, 실행 이력이 없으면 null) | +| next_run_at | string | `2026-07-07T12:00:00+09:00` | next run 일시 | +| creator | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| created_at | string | `2026-07-06` | 생성 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 예약 작업(스케줄) 목록을 페이지네이션과 함께 조회하며, 응답에는 상태별 통계도 포함됩니다. `core.schedules.read` 권한이 필요합니다. `type`/`frequency`/`status`/`last_result`/`extension_type` 등으로 필터링하고 `sort_by`/`sort_order` 로 정렬할 수 있습니다. 관리자 스케줄 관리 화면의 목록·필터·요약 카드 표시에 사용합니다. + + +### POST /api/admin/schedules + +- **라우트명**: `api.admin.schedules.store` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ScheduleController@store` +- **인증/권한**: `auth:sanctum` + `permission:core.schedules.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | string | 예 | max 255 | 대상의 이름/명칭 | +| description | body | string | 아니오 | max 1000 | 설명 | +| type | body | string | 예 | `artisan`, `shell`, `url` | 작업 유형: artisan(Artisan 커맨드 실행), shell(쉘 명령 실행), url(URL 호출) | +| command | body | string | 예 | max 2000 | 실행할 아티즌 커맨드 | +| expression | body | string | 예 | max 100 | 실행 시각을 정의하는 Cron 표현식 (예: `0 3 * * *`, 다음 실행 시각 next_run_at 계산의 기준) | +| frequency | body | string | 예 | `everyMinute`, `hourly`, `daily`, `weekly`, `monthly`, `custom` | 실행 주기 | +| without_overlapping | body | boolean | 아니오 | — | 중복 실행 방지 여부 | +| run_in_maintenance | body | boolean | 아니오 | — | 점검 모드 중 실행 여부 | +| timeout | body | integer | 아니오 | min 1, max 86400 | 타임아웃 (초) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| extension_type | body | string | 아니오 | `core`, `module`, `plugin` | 확장 유형 (core/module/plugin/template) | +| extension_identifier | body | string | 아니오 | max 255 | 확장 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.schedule.create_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 새 예약 작업을 생성합니다. `core.schedules.create` 권한이 필요합니다. `type`(artisan/shell/url), `command`, `expression`, `frequency` 를 지정하며 생성자(creator)는 현재 사용자로 자동 기록됩니다. 검증 실패 시 422로 응답하고, 성공 시 201과 생성된 스케줄 리소스를 반환합니다. 관리자 스케줄 등록 폼에 사용합니다. + + +### DELETE /api/admin/schedules/bulk + +- **라우트명**: `api.admin.schedules.bulk-delete` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ScheduleController@bulkDelete` +- **인증/권한**: `auth:sanctum` + `permission:core.schedules.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | query | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.schedule.bulk_delete_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.delete`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 전달된 `ids` 배열의 스케줄을 일괄 삭제합니다. `core.schedules.delete` 권한이 필요합니다. 삭제 처리 결과 요약을 반환하며, 검증 실패 시 422로 응답합니다. 목록에서 여러 스케줄을 선택해 한 번에 제거하는 동작에 사용합니다. + + +### PATCH /api/admin/schedules/bulk-status + +- **라우트명**: `api.admin.schedules.bulk-status` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ScheduleController@bulkUpdateStatus` +- **인증/권한**: `auth:sanctum` + `permission:core.schedules.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| is_active | body | boolean | 예 | — | 활성 여부 (true 활성 / false 비활성) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.schedule.bulk_update_status_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 전달된 `ids` 배열의 스케줄 활성 상태(`is_active`)를 일괄 변경합니다. `core.schedules.update` 권한이 필요합니다. 처리 결과 요약을 반환하며, 검증 실패 시 422로 응답합니다. 목록에서 여러 스케줄을 선택해 한 번에 활성화/비활성화하는 동작에 사용합니다. + + +### DELETE /api/admin/schedules/history/{historyId} + +- **라우트명**: `api.admin.schedules.delete-history` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ScheduleController@deleteHistory` +- **인증/권한**: `auth:sanctum` + `permission:core.schedules.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| historyId | path | string | 예 | — | 대상 history의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 실행 이력 레코드를 삭제합니다. `core.schedules.delete` 권한이 필요합니다. 대상 이력이 없으면 404로 응답합니다. 스케줄 상세의 실행 이력 목록에서 개별 이력을 제거할 때 사용합니다. + + +### GET /api/admin/schedules/statistics + +- **라우트명**: `api.admin.schedules.statistics` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ScheduleController@statistics` +- **인증/권한**: `auth:sanctum` + `permission:core.schedules.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| total | integer | `1` | 전체 개수 (집계) | +| active | integer | `1` | 활성(is_active=true) 스케줄 수 | +| inactive | integer | `0` | 비활성(is_active=false) 스케줄 수 | +| success | integer | `1` | 마지막 실행 결과가 성공(success)인 스케줄 수 | +| failed | integer | `0` | 마지막 실행 결과가 실패(failed)인 스케줄 수 | +| running | integer | `0` | 마지막 실행 결과가 실행중(running)인 스케줄 수 | +| never_run | integer | `0` | 아직 한 번도 실행되지 않은(last_result=never) 스케줄 수 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.read`)이 없는 경우 | + + + +**설명** 스케줄 전체의 집계 통계를 조회합니다. `core.schedules.read` 권한이 필요합니다. 전체/활성/비활성 수와 마지막 실행 결과별(성공/실패/실행중/미실행) 건수를 반환합니다. 관리자 스케줄 대시보드의 요약 카드 표시에 사용합니다. + + +### DELETE /api/admin/schedules/{schedule} + +- **라우트명**: `api.admin.schedules.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ScheduleController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:core.schedules.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| schedule | path | string | 예 | — | 대상 schedule의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 단일 스케줄을 삭제합니다. `core.schedules.delete` 권한이 필요합니다. 경로의 `schedule` 은 라우트 모델 바인딩으로 해석되어 존재하지 않으면 404가 됩니다. 관리자 스케줄 관리 화면의 개별 삭제 동작에 사용합니다. + + +### GET /api/admin/schedules/{schedule} + +- **라우트명**: `api.admin.schedules.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ScheduleController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.schedules.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| schedule | path | string | 예 | — | 대상 schedule의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| 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 | `true` | active 여부 | +| last_result | string | `success` | 마지막 실행 결과: success(성공), failed(실패), running(실행중), never(미실행) | +| last_result_label | string | `성공` | `last_result` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| last_run_at | string | `2026-07-05T19:20:23+09:00` | last run 일시 | +| last_duration | null | `null` | 마지막 실행의 소요 시간을 사람이 읽는 문자열로 포맷한 값 (예: "45초", "2분 3초" — 마지막 실행 이력의 duration 파생, 실행 이력이 없으면 null) | +| next_run_at | string | `2026-07-07T12:00:00+09:00` | next run 일시 | +| extension_type | null | `null` | 확장 소유 타입: core(코어), module(모듈), plugin(플러그인), NULL(사용자 정의) | +| extension_identifier | null | `null` | 확장 식별자 (예: core, sirsoft-board, sirsoft-payment) | +| creator | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| created_at | string | `2026-07-06 19:20:23` | 생성 일시 | +| 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 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 단일 스케줄의 상세 정보를 조회하며 생성자(creator) 정보를 함께 로드합니다. `core.schedules.read` 권한이 필요합니다. 경로의 `schedule` 은 라우트 모델 바인딩으로 해석되어 없으면 404가 됩니다. 관리자 스케줄 상세/수정 화면의 초기 데이터 로딩에 사용합니다. + + +### PUT /api/admin/schedules/{schedule} + +- **라우트명**: `api.admin.schedules.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ScheduleController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.schedules.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| schedule | path | string | 예 | — | 대상 schedule의 식별자 | +| name | body | string | 예 | max 255 | 대상의 이름/명칭 | +| description | body | string | 아니오 | max 1000 | 설명 | +| type | body | string | 예 | `artisan`, `shell`, `url` | 작업 유형: artisan(Artisan 커맨드 실행), shell(쉘 명령 실행), url(URL 호출) | +| command | body | string | 예 | max 2000 | 실행할 아티즌 커맨드 | +| expression | body | string | 예 | max 100 | 실행 시각을 정의하는 Cron 표현식 (예: `0 3 * * *`, 다음 실행 시각 next_run_at 계산의 기준) | +| frequency | body | string | 예 | `everyMinute`, `hourly`, `daily`, `weekly`, `monthly`, `custom` | 실행 주기 | +| without_overlapping | body | boolean | 아니오 | — | 중복 실행 방지 여부 | +| run_in_maintenance | body | boolean | 아니오 | — | 점검 모드 중 실행 여부 | +| timeout | body | integer | 아니오 | min 1, max 86400 | 타임아웃 (초) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| extension_type | body | string | 아니오 | `core`, `module`, `plugin` | 확장 유형 (core/module/plugin/template) | +| extension_identifier | body | string | 아니오 | max 255 | 확장 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.schedule.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 기존 스케줄 정보를 수정합니다. `core.schedules.update` 권한이 필요합니다. store 와 동일한 필드(`type`/`command`/`expression`/`frequency` 등)를 받으며 검증 실패 시 422로 응답합니다. 경로의 `schedule` 은 라우트 모델 바인딩으로 해석되며, 수정된 스케줄 리소스를 반환합니다. 관리자 스케줄 수정 폼에 사용합니다. + + +### POST /api/admin/schedules/{schedule}/duplicate + +- **라우트명**: `api.admin.schedules.duplicate` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ScheduleController@duplicate` +- **인증/권한**: `auth:sanctum` + `permission:core.schedules.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| schedule | path | string | 예 | — | 대상 schedule의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.create`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 기존 스케줄을 복제해 새 스케줄을 만듭니다. `core.schedules.create` 권한이 필요합니다. 원본을 바탕으로 새 레코드를 생성하며, 성공 시 201과 복제된 스케줄 리소스를 반환합니다. 유사한 설정의 스케줄을 빠르게 추가하는 "복제" 동작에 사용합니다. + + +### GET /api/admin/schedules/{schedule}/history + +- **라우트명**: `api.admin.schedules.history` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ScheduleController@history` +- **인증/권한**: `auth:sanctum` + `permission:core.schedules.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| schedule | path | string | 예 | — | 대상 schedule의 식별자 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| status | query | string | 아니오 | `success`, `failed`, `running` | 상태 필터 (해당 상태의 항목만 조회) | +| trigger_type | query | string | 아니오 | `scheduled`, `manual` | 실행 방식 필터: scheduled(예약 시각 자동 실행), manual(관리자의 즉시 실행) 중 해당 이력만 조회 | +| started_from | query | date | 아니오 | — | 실행 시작일 범위의 시작일 (이 날짜 이후 시작된 이력만 조회) | +| started_to | query | date | 아니오 | — | 실행 시작일 범위의 종료일 (이 날짜 이전 시작된 이력만 조회, started_from 이후여야 함) | +| sort_by | query | string | 아니오 | `started_at`, `ended_at`, `duration`, `status` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.schedule.history_list_validation_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 스케줄의 실행 이력을 페이지네이션으로 조회합니다. `core.schedules.read` 권한이 필요합니다. `status`(success/failed/running), `trigger_type`(scheduled/manual), 기간(`started_from`/`started_to`)으로 필터링하고 `sort_by`/`sort_order` 로 정렬할 수 있습니다. 스케줄 상세의 실행 이력 탭 표시에 사용합니다. + + +### POST /api/admin/schedules/{schedule}/run + +- **라우트명**: `api.admin.schedules.run` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\ScheduleController@run` +- **인증/권한**: `auth:sanctum` + `permission:core.schedules.run` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| schedule | path | string | 예 | — | 대상 schedule의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.run`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 스케줄을 예약 시각과 무관하게 즉시 실행합니다. `core.schedules.run` 권한이 필요합니다. 실제 명령이 수행되고 실행 이력 레코드가 생성되며, 그 이력(trigger_type=manual)을 반환합니다. 관리자가 대상 작업을 수동으로 즉시 돌려 결과를 확인하는 "지금 실행" 동작에 사용합니다. + + diff --git a/docs/backend/api/search.md b/docs/backend/api/search.md new file mode 100644 index 00000000..e74626a3 --- /dev/null +++ b/docs/backend/api/search.md @@ -0,0 +1,61 @@ +# Search API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Search 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/search + +- **라우트명**: `api.search` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicSearchController@search` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| q | query | string | 아니오 | min 2, max 200 | 검색어 (부분 일치) | +| type | query | string | 아니오 | — | 유형 필터 (해당 유형의 항목만 조회) | +| sort | query | string | 아니오 | `relevance`, `latest`, `oldest`, `views`, `popular`, `price_asc`, `price_desc` | 정렬 기준 (필드명, `-` 접두 시 내림차순) | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| board_slug | query | string | 아니오 | max 100 | 검색 범위를 특정 게시판으로 한정 (게시판 모듈이 `core.search.validation_rules` 훅으로 추가하는 파라미터, 해당 slug의 게시판 글만 검색) | +| category_id | query | integer | 아니오 | — | category 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.search.validation_rules`). + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| q | string | `` | 실제 검색에 사용된 검색어 (요청 `q` 를 trim 하여 에코, 검색어가 비어 있으면 빈 문자열) | +| total | integer | `0` | 전체 개수 (집계) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +프론트엔드 통합 검색(`search/index.json`)이 호출하는 공개 엔드포인트입니다. 인증이 필요 없으며 게스트도 사용할 수 있습니다. 코어 컨트롤러는 검색 결과를 직접 생성하지 않고, 검증된 파라미터로 검색 컨텍스트(q/type/sort/page/per_page 및 요청 객체)를 구성한 뒤 `core.search.results` Filter 훅을 실행합니다. 게시판·상품 등 각 검색 대상 모듈이 이 훅에 리스너를 등록해 자신의 카테고리 결과를 추가하고, `core.search.build_response` 훅으로 응답 구조를 완성합니다. 따라서 활성 검색 모듈이 없으면 항상 빈 결과(`total: 0`)가 반환됩니다. 검색 엔진 자체는 Scout + `DatabaseFulltextEngine`(MySQL FULLTEXT) 기반이며, 상세는 `docs/backend/search-system.md`를 참고하세요. + + diff --git a/docs/backend/api/seo.md b/docs/backend/api/seo.md new file mode 100644 index 00000000..bd566602 --- /dev/null +++ b/docs/backend/api/seo.md @@ -0,0 +1,174 @@ +# Seo API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Seo 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/seo/cached-urls + +- **라우트명**: `api.admin.seo.cached-urls` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SeoCacheController@cachedUrls` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| urls | array | `[]` | 현재 사전 렌더 캐시에 남아 있는 봇 대상 페이지 URL 목록 (SeoCacheManager 인덱스에서 조회). | +| count | integer | `0` | 캐시된 URL 개수 (`urls` 배열 길이). | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | + + + +**설명** + +SeoCacheManager 인덱스에서 현재 캐시된 SEO 페이지 URL 목록과 개수를 조회합니다. 어떤 봇 대상 페이지가 사전 렌더 캐시로 남아 있는지 확인하는 진단 용도로 사용합니다. + + +### POST /api/admin/seo/clear-cache + +- **라우트명**: `api.admin.seo.clear-cache` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SeoCacheController@clearCache` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| layout | body | string | 아니오 | — | 무효화 대상 레이아웃명. 지정 시 해당 레이아웃의 SEO 캐시만 삭제하고 무효화된 항목 수를 반환하며, 미지정 시 전체 SEO 캐시를 삭제한다. | +| module | body | string | 아니오 | — | 모듈 식별자 필터. 검증 규칙에는 정의되어 있으나 현재 컨트롤러 로직에서는 사용되지 않는다(향후 모듈 단위 캐시 무효화 확장 예약 필드). | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +SEO 사전 렌더 캐시를 삭제합니다. `layout` 을 지정하면 해당 레이아웃 캐시만 무효화하고, 지정하지 않으면 전체 SEO 캐시를 삭제합니다. 응답의 `data.cleared` 는 `layout` 지정 시 무효화된 항목 수(정수), 미지정 시 문자열 `"all"` 입니다. 설정이나 콘텐츠 변경 후 오래된 봇 응답이 캐시로 남는 것을 방지할 때 사용합니다. + + +### POST /api/admin/seo/sitemap/regenerate + +- **라우트명**: `api.admin.seo.sitemap.regenerate` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SeoCacheController@regenerateSitemap` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | + + + +**설명** + +sitemap.xml 을 즉시 재생성합니다. SitemapManager 에 위임하며 큐 드라이버와 무관하게 동기(즉시) 실행되고, 완료 후 마지막 생성 시각을 갱신합니다. SEO 설정에서 sitemap 기능이 비활성인 경우 400(`seo.sitemap_disabled`), 생성 실패 시 500 을 반환합니다. 관리자가 콘텐츠 변경 후 검색엔진에 노출할 sitemap 을 스케줄 대기 없이 즉시 갱신할 때 사용합니다. + + +### GET /api/admin/seo/stats + +- **라우트명**: `api.admin.seo.stats` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SeoCacheController@stats` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| overall | object | `{"total_entries":0,"hits":0,"misses":0,"hit_rate":0,"avg_…` | 최근 7일 전체 캐시 통계 집계. `total_entries`(기록 총 건수), `hits`(적중 건수), `misses`(미적중 건수), `hit_rate`(적중률 %, hits/total×100), `avg_response_time_ms`(미적중 시 평균 렌더링 소요 시간 ms, 데이터 없으면 null). | +| 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 페이지가 캐시로 재사용되는지 파악하는 용도. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | + + + +**설명** + +SEO 캐시 적중 현황을 최근 7일 기준으로 전체·레이아웃별·모듈별로 조회합니다. 봇 대상 사전 렌더 캐시가 얼마나 효과적으로 재사용되는지(적중률, 렌더 비용 절감)를 모니터링하는 관리자 대시보드용 통계입니다. + + +### POST /api/admin/seo/warmup + +- **라우트명**: `api.admin.seo.warmup` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SeoCacheController@warmup` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | + + + +**설명** + +SEO 캐시 워밍업(모든 SEO 레이아웃 사전 렌더)을 위한 엔드포인트입니다. 현재 컨트롤러는 실제 워밍업 로직 없이 `status: dispatched` 와 안내 메시지만 반환합니다(실 렌더링은 후속 구현 예정). 응답 성공은 요청 접수만을 의미하며 이 시점에 캐시가 채워지지는 않습니다. + + diff --git a/docs/backend/api/settings.md b/docs/backend/api/settings.md new file mode 100644 index 00000000..a12de377 --- /dev/null +++ b/docs/backend/api/settings.md @@ -0,0 +1,563 @@ +# Settings API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Settings 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/settings + +- **라우트명**: `api.admin.settings.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| general | object | `{"site_name":"Test Site","site_url":"https:\/\/test.examp…` | 일반 탭 설정 그룹 (사이트명·사이트 URL·설명·관리자 이메일·타임존·기본 언어·통화·점검 모드·사이트 로고 첨부). site_logo 는 SettingsService 가 별도 주입한 첨부 정보 | +| security | object | `{"force_https":true,"login_attempt_enabled":true,"auth_to…` | 보안 탭 설정 그룹 (HTTPS 강제·로그인 시도 제한 사용·인증 토큰 유지시간(분, 0=무한)·최대 로그인 시도 횟수·잠금 시간) | +| mail | object | `{"mailer":"smtp","host":"","port":587,"username":"","pass…` | 메일 탭 설정 그룹 (메일러 종류(smtp/mailgun/ses)·SMTP 호스트/포트/인증 정보·암호화 방식·발신자 주소/이름·Mailgun/SES 자격 정보) | +| upload | object | `{"max_file_size":10,"allowed_extensions":["jpg","jpeg","p…` | 업로드 탭 설정 그룹 (최대 파일 크기(MB)·허용 확장자 목록·이미지 최대 가로/세로·이미지 품질) | +| seo | object | `{"meta_title_suffix":"","meta_description":"","meta_keywo…` | SEO 탭 설정 그룹 (메타 타이틀 접미사·메타 설명/키워드·검색엔진 인증 코드·봇 감지·OG/Twitter 기본값·SEO 캐시·사이트맵·생성기 설정) | +| advanced | object | `{"cache_enabled":true,"cache_default_ttl":86400,"layout_c…` | 고급 탭 설정 그룹 (캐시·디버그·코어 업데이트·GeoIP 설정을 한 탭으로 합친 병합 뷰). cache/debug 카테고리 값이 함께 노출됨 | +| cache | object | `{"cache_enabled":true,"cache_default_ttl":86400,"layout_c…` | 캐시 원본 카테고리 (전역 캐시 사용·기본 TTL·레이아웃/통계/SEO 캐시 사용 및 TTL). advanced 탭에 병합되면서 개별 접근용으로 별도 노출된 파생 뷰 | +| debug | object | `{"debug_mode":true,"sql_query_log":false,"log_level":"err…` | 디버그 원본 카테고리 (디버그 모드·SQL 쿼리 로그·로그 레벨). advanced 탭에 병합되면서 개별 접근용으로 별도 노출된 파생 뷰 | +| drivers | object | `{"storage_driver":"local","s3_bucket":null,"s3_region":"a…` | 드라이버 탭 설정 그룹 (스토리지/캐시/세션/큐/로그 드라이버 선택 + S3·Redis·Memcached·WebSocket·검색엔진 접속 파라미터) | +| core_update | object | `{"core_update_github_url":"https:\/\/github.com\/custom\/…` | 코어 업데이트 원본 카테고리 (코어 업데이트를 받아올 GitHub 저장소 URL·비공개 저장소 접근용 토큰). advanced 탭에 병합된 파생 뷰 | +| geoip | object | `{"geoip_enabled":false,"geoip_license_key":null,"geoip_au…` | GeoIP 원본 카테고리 (GeoIP 사용 여부·MaxMind 라이선스 키·DB 자동 갱신 사용). advanced 탭에 병합된 파생 뷰 | +| notifications | object | `{"channels":[{"id":"mail","is_active":true,"sort_order":1…` | 알림 탭 설정 그룹. channels 는 알림 채널 목록으로 각 원소가 id(채널 식별자)·is_active(활성 여부)·sort_order(표시 순서)를 가짐 | +| identity | object | `{"default_provider":"g7:core.mail","purpose_providers":{"…` | 본인인증(IDV) 탭 설정 그룹 (기본 provider·목적별 provider 매핑(purpose_providers)·챌린지 유효시간(분)·최대 시도 횟수) | +| 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 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | + + + +**설명** + +관리자 통합 환경설정 화면(`admin_settings.json`)이 사용하는 전체 설정 조회 엔드포인트입니다. 각 탭에 해당하는 설정 그룹(general/security/mail/upload/seo/advanced/drivers/geoip/notifications/identity 등)과 드라이버 선택지 카탈로그(`available_drivers`)를 한 번에 반환합니다. 응답은 Eloquent 모델이 아니라 SettingsService 가 여러 설정 소스를 병합해 만든 집계 배열이며, 일부 그룹(cache/debug 등)은 원본 카테고리 값을 별도 키로 함께 노출한 파생 뷰입니다. + + +### POST /api/admin/settings + +- **라우트명**: `api.admin.settings.store` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@store` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| _tab | body | string | 아니오 | — | 활성 탭 식별자 (general/mail/upload/seo/security/drivers/advanced/notifications/identity). 지정 시 해당 탭 필드만 필수 검증되고 나머지 탭은 nullable 처리되어 탭 단위 부분 저장을 가능케 함 | +| general | body | array | 아니오 | — | 일반 탭 설정 묶음 (사이트명·URL·설명·관리자 이메일·타임존·기본 언어·통화·점검 모드·사이트 로고) | +| mail | body | array | 아니오 | — | 메일 탭 설정 묶음 (메일러 종류·SMTP 호스트/포트/인증·암호화·발신자 정보·Mailgun/SES 자격 정보) | +| upload | body | array | 아니오 | — | 업로드 탭 설정 묶음 (최대 파일 크기·허용 확장자·이미지 최대 크기 및 품질) | +| seo | body | array | 아니오 | — | SEO 탭 설정 묶음 (메타 태그·검색엔진 인증·봇 감지·OG/Twitter 기본값·SEO 캐시·사이트맵·생성기) | +| security | body | array | 아니오 | — | 보안 탭 설정 묶음 (HTTPS 강제·로그인 시도 제한·인증 토큰 유지시간·최대 시도 횟수·잠금 시간) | +| drivers | body | array | 아니오 | — | 드라이버 탭 설정 묶음 (스토리지/캐시/세션/큐/로그 드라이버 및 S3·Redis·Memcached·WebSocket·검색엔진 접속 정보) | +| advanced | body | array | 아니오 | — | 고급 탭 설정 묶음 (캐시·디버그·코어 업데이트·GeoIP 설정) | +| notifications | body | array | 아니오 | — | 알림 탭 설정 묶음. channels 배열로 각 알림 채널의 id·is_active(활성 여부)·sort_order(표시 순서)를 저장 | +| identity | body | array | 아니오 | — | 본인인증(IDV) 탭 설정 묶음 (기본 provider·목적별 provider 매핑·챌린지 유효시간·최대 시도 횟수) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.save_validation_rules`, `core.search.engine_drivers`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +통합 환경설정 화면에서 한 탭의 설정을 일괄 저장합니다. `_tab` 으로 활성 탭을 지정하면 해당 탭의 필드만 필수 검증되고 다른 탭 필드는 nullable 로 처리되므로, 탭 단위로 부분 저장할 수 있습니다. 저장 성공 시 응답 `data.settings` 에 갱신된 전체 설정과 `available_drivers` 를 함께 반환하여, 프론트엔드가 새로고침 없이 전역 상태를 갱신할 수 있습니다. 검증 실패 시 422, 그 외 오류 시 500 을 반환합니다. + + +### GET /api/admin/settings/app-key + +- **라우트명**: `api.admin.settings.app-key` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@getAppKey` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| app_key | string | `base64:97gZH*************************…` | 현재 애플리케이션 키(`APP_KEY`)를 마스킹한 문자열. 앞부분 일부만 노출하고 나머지는 별표로 가려 전체 원문은 반환하지 않음 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | + + + +**설명** + +현재 애플리케이션 키(`APP_KEY`)를 마스킹된 형태로 조회합니다. 관리자 화면에서 앱 키 존재/일부만 표시하는 용도이며, 전체 키 원문은 반환하지 않습니다. + + +### POST /api/admin/settings/backup + +- **라우트명**: `api.admin.settings.backup` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@backup` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | + + + +**설명** + +현재 설정을 백업 파일로 저장합니다. 응답 `data.backup_path` 에 생성된 백업 경로를 반환하며, 이 경로는 이후 `POST /restore` 의 `backup_path` 로 사용할 수 있습니다. 설정 변경 전 스냅샷을 남길 때 사용합니다. + + +### POST /api/admin/settings/backup-database + +- **라우트명**: `api.admin.settings.backup-database` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@backupDatabase` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | + + + +**설명** + +데이터베이스를 백업합니다. SettingsService 에 위임하며, 성공/실패를 메시지로 반환합니다. 설정 백업(`POST /backup`)이 설정 파일만 다루는 것과 달리, 이 엔드포인트는 DB 데이터를 백업 대상으로 합니다. + + +### POST /api/admin/settings/clear-cache + +- **라우트명**: `api.admin.settings.clear-cache` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@clearCache` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | + + + +**설명** + +시스템 캐시를 정리합니다. 시스템 정보 캐시를 지원 로케일별로 비운 뒤 `cache:clear`, `route:clear`, `view:clear` 를 실행하고, config 캐시는 비운 직후 즉시 재생성합니다(비워 두면 이후 모든 요청이 config 를 재파싱하므로). 설정/코드 변경 후 오래된 캐시를 초기화할 때 사용합니다. + + +### POST /api/admin/settings/geoip/update + +- **라우트명**: `api.admin.settings.geoip.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\GeoIpController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | + + + +**설명** + +MaxMind GeoLite2-City DB 를 즉시 재다운로드합니다. GeoIpDatabaseService 에 위임하며 동기(즉시) 실행되므로 웹서버/PHP-FPM 타임아웃(90초 이상)이 필요합니다. 라이선스 키 미설정 시 400, 키가 잘못된 경우 401, 연결 실패/기타 오류 시 500 을 반환합니다. 정기 갱신은 스케줄(`geoip:update`)이 담당하고, 이 엔드포인트는 수동 갱신 트리거입니다. + + +### POST /api/admin/settings/optimize-system + +- **라우트명**: `api.admin.settings.optimize-system` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@optimizeSystem` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | + + + +**설명** + +시스템을 최적화합니다. `config:cache`, `route:cache`, `view:cache` 를 실행해 설정·라우트·뷰 캐시를 생성함으로써 이후 요청의 부팅 비용을 줄입니다. 캐시를 비우는 `clear-cache` 와 반대로, 캐시를 사전 생성하는 프로덕션 성능용 작업입니다. + + +### POST /api/admin/settings/regenerate-app-key + +- **라우트명**: `api.admin.settings.regenerate-app-key` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@regenerateAppKey` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| password | body | string | 예 | — | 비밀번호 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.regenerate_app_key_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +애플리케이션 키(`APP_KEY`)를 재생성합니다. FormRequest 단계에서 `super_admin` 역할만 허용하고, Service 단계에서 요청자 본인의 비밀번호가 일치하는지 다시 확인합니다(불일치 시 401). 성공 시 새 키를 `.env` 의 `APP_KEY` 에 기록하고 config 캐시를 재생성하며, 응답 `data.app_key` 에 새 키를 반환합니다. 앱 키 변경은 기존 암호화 값/서명 무효화를 동반하므로 주의가 필요합니다. + + +### POST /api/admin/settings/restore + +- **라우트명**: `api.admin.settings.restore` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@restore` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| backup_path | body | string | 예 | — | 복원할 백업 파일 경로. `POST /api/admin/settings/backup` 응답의 `backup_path` 로 받은 값을 그대로 지정 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.restore_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +이전에 만든 설정 백업에서 설정을 복원합니다. `backup_path` 로 `POST /backup` 이 반환한 백업 경로를 지정합니다. 복원 성공 시 시스템 설정 캐시를 무효화합니다. 잘못된 설정을 되돌릴 때 사용합니다. + + +### GET /api/admin/settings/system-info + +- **라우트명**: `api.admin.settings.system-info` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@systemInfo` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| os_info | string | `Windows NT 10.0` | 운영체제 종류와 버전 (`php_uname` 산물). probe 차단 시 "알 수 없음" 폴백 | +| web_server | string | `Apache/2.4.62 (Win64) OpenSSL/3.0.16 …` | 웹서버 소프트웨어 식별 문자열 (`$_SERVER['SERVER_SOFTWARE']`) | +| php_version | string | `8.3.26` | 실행 중인 PHP 버전 (`PHP_VERSION`) | +| mysql_version | string | `Mysql 8.4.3` | 연결된 데이터베이스 서버 종류와 버전 (DB 조회 산물). probe 실패 시 "알 수 없음" 폴백 | +| g7_version | string | `7.0.1` | G7 코어 버전 (`config('app.version')`) | +| g7_release_year | string | `2026` | G7 릴리즈 연도 (`config('app.release_year')`, 저작권 표기 등에 사용) | +| laravel_version | string | `12.54.1` | 프레임워크 Laravel 버전 (`app()->version()`) | +| environment | string | `local` | 현재 실행 환경 (`app()->environment()` — local/production/testing 등) | +| cpu_info | string | `Intel(R) Core(TM) Ultra 5 225H` | CPU 모델명 (OS별 시스템 probe 산물). 수집 실패 시 "알 수 없음" 폴백 | +| memory_usage | object | `{"total":"31.49 GB","used":"29.63 GB","free":"1.86 GB","p…` | 물리 메모리 사용량. total/used/free 는 사람이 읽기 쉬운 단위 문자열, percentage 는 사용률(%) | +| disk_usage | object | `{"total":"474.72 GB","used":"360.2 GB","free":"114.51 GB"…` | 설치 볼륨 디스크 사용량. total/used/free 단위 문자열 + percentage 사용률(%) | +| php_memory_limit | string | `512M` | PHP `memory_limit` ini 값 | +| max_execution_time | string | `36000초` | PHP `max_execution_time` ini 값 (초 단위 접미사 부착) | +| upload_max_filesize | string | `2G` | PHP `upload_max_filesize` ini 값 | +| install_path | string | `C:\Users\HeuJung\htdocs\g7_2` | 애플리케이션 설치 루트 경로 (`base_path()`) | +| config_path | string | `C:\Users\HeuJung\htdocs\g7_2\storage\…` | 설정 파일 저장 경로 (`storage/app/settings`) | +| log_path | string | `C:\Users\HeuJung\htdocs\g7_2\storage\…` | 로그 파일 저장 경로 (`storage/logs`) | +| upload_path | string | `C:\Users\HeuJung\htdocs\g7_2\storage\…` | 공개 업로드 파일 저장 경로 (`storage/app/public`) | +| php_extensions | object | `{"required":{"openssl":true,"pdo":true,"mbstring":true,"t…` | PHP 확장 로드 상태. required(필수)·optional(선택) 두 그룹으로 나뉘며 각 확장명→로드 여부(bool) 매핑 | +| database_config | object | `{"has_read_write_split":false,"write":{"host":"localhost"…` | DB 연결 구성 요약. has_read_write_split(읽기/쓰기 분리 여부)·write(쓰기 연결 정보)·read(읽기 replica 목록, write 와 동일하면 제외) | +| timezone | string | `UTC` | 애플리케이션 기본 타임존 (`config('app.timezone')`) | +| server_time | string | `2026-07-07 05:08:58` | 서버 현재 시각 (Y-m-d H:i:s) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | + + + +**설명** + +서버 실행 환경 정보를 한 번에 조회합니다. OS/웹서버/PHP/DB/Laravel/코어 버전, CPU·메모리·디스크 사용량, PHP 주요 설정값(memory_limit·max_execution_time·upload_max_filesize), 주요 경로, PHP 확장 로드 상태, DB 연결 구성 요약 등을 포함합니다. 관리자 시스템 정보 화면과 요구사항 점검용 진단 데이터로 사용됩니다. + + +### POST /api/admin/settings/test-driver + +- **라우트명**: `api.admin.settings.test-driver` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@testDriverConnection` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| storage_driver | body | string | 아니오 | — | 스토리지 드라이버 (local/s3) | +| cache_driver | body | string | 아니오 | — | 캐시 드라이버 (file/redis/memcached) | +| session_driver | body | string | 아니오 | — | 세션 드라이버 (file/database/redis) | +| queue_driver | body | string | 아니오 | — | 큐 드라이버 (sync/database/redis) | +| websocket_enabled | body | boolean | 아니오 | — | WebSocket 사용 여부 | +| s3_bucket | body | string | 아니오 | max 255 | S3 버킷명 | +| s3_region | body | string | 아니오 | — | S3 리전 | +| s3_access_key | body | string | 아니오 | max 255 | S3 액세스 키 | +| s3_secret_key | body | string | 아니오 | max 255 | S3 시크릿 키 | +| s3_url | body | string | 아니오 | max 500 | S3 엔드포인트 URL | +| redis_host | body | string | 아니오 | max 255 | Redis 호스트 주소 | +| redis_port | body | integer | 아니오 | min 1, max 65535 | Redis 포트 번호 | +| redis_password | body | string | 아니오 | max 255 | Redis 비밀번호 | +| redis_database | body | integer | 아니오 | min 0, max 15 | Redis 데이터베이스 번호 | +| memcached_host | body | string | 아니오 | max 255 | Memcached 호스트 주소 | +| memcached_port | body | integer | 아니오 | min 1, max 65535 | Memcached 포트 번호 | +| websocket_app_key | body | string | 아니오 | max 255 | WebSocket 앱 키 | +| websocket_host | body | string | 아니오 | max 255 | WebSocket 호스트 주소 | +| websocket_port | body | integer | 아니오 | min 1, max 65535 | WebSocket 포트 번호 | +| websocket_scheme | body | string | 아니오 | — | WebSocket 스킴 (http/https) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.test_driver_connection_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +폼에 입력한 드라이버 접속 정보(S3·Redis·Memcached·Websocket 등)로 실제 연결을 시도해 결과를 반환합니다. 설정을 저장하기 전에 접속 정보가 유효한지 확인하는 용도입니다. 모든 테스트 통과 시 성공 메시지, 일부 실패 시에도 HTTP 성공 응답으로 항목별 결과(`all_passed=false` 포함)를 함께 반환합니다. + + +### POST /api/admin/settings/test-mail + +- **라우트명**: `api.admin.settings.test-mail` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@testMail` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| to_email | body | email | 예 | max 255 | 테스트 수신 주소 | +| mailer | body | string | 아니오 | `smtp`, `mailgun`, `ses` | 메일 발송 드라이버 (smtp/mailgun/ses) | +| from_address | body | email | 예 | max 255 | 발신자 주소 | +| from_name | body | string | 예 | max 255 | 발신자 이름 | +| host | body | string | 예 | max 255 | 호스트 주소 | +| port | body | integer | 예 | min 1, max 65535 | 포트 번호 | +| username | body | string | 아니오 | max 255 | 사용자명 (로그인/인증 아이디) | +| password | body | string | 아니오 | max 255 | 비밀번호 | +| encryption | body | string | 아니오 | `tls`, `ssl`, `null` | 전송 암호화 방식 (tls/ssl) | +| mailgun_domain | body | string | 아니오 | max 255 | Mailgun 도메인 | +| mailgun_secret | body | string | 아니오 | max 255 | Mailgun 시크릿 키 | +| mailgun_endpoint | body | string | 아니오 | max 255 | Mailgun 엔드포인트 | +| ses_key | body | string | 아니오 | max 255 | SES 액세스 키 | +| ses_secret | body | string | 아니오 | max 255 | SES 시크릿 키 | +| ses_region | body | string | 아니오 | max 255 | SES 리전 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.test_mail_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +폼에 입력한 메일 설정으로 지정한 주소에 테스트 메일을 발송합니다. 요청에서 전달한 값(호스트·포트·인증 정보 등)을 저장된 메일 설정 위에 임시로 덮어써 그 값으로만 발송을 시도하므로, 설정을 저장하기 전에 실제 발송 가능 여부를 검증할 수 있습니다. 성공 시 발송한 제목/본문을 응답에 포함하고, 실패 시 오류 사유와 함께 500 을 반환합니다. + + +### GET /api/admin/settings/{key} + +- **라우트명**: `api.admin.settings.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| key | path | string | 예 | — | 대상 설정/항목의 키 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +단일 설정 키의 값을 조회합니다. 응답의 `data.key` 는 요청한 키, `data.value` 는 해당 설정 값입니다. 통합 조회(`GET /api/admin/settings`)와 달리 특정 키 하나만 필요할 때 사용합니다. + + +### PUT /api/admin/settings/{key} + +- **라우트명**: `api.admin.settings.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| key | path | string | 예 | — | 대상 설정/항목의 키 | +| value | body | string | 예 | max 1000 | 값 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +단일 설정 키의 값을 업데이트합니다. 경로의 `key` 로 대상 설정을, 본문의 `value` 로 새 값을 지정합니다. 탭 단위 일괄 저장(`POST /api/admin/settings`)과 달리 개별 키 하나만 변경할 때 사용합니다. 검증 실패 시 422, 그 외 오류 시 500 을 반환합니다. + + diff --git a/docs/backend/api/templates.md b/docs/backend/api/templates.md new file mode 100644 index 00000000..da840f0a --- /dev/null +++ b/docs/backend/api/templates.md @@ -0,0 +1,1810 @@ +# Templates API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Templates 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/templates + +- **라우트명**: `api.admin.templates.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| type | query | string | 아니오 | `user`, `admin` | 유형 필터 (해당 유형의 항목만 조회) | +| search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| filters | query | array | 아니오 | max 10 | 추가 필터 조건 맵 (필드별 조건) | +| status | query | string | 아니오 | `installed`, `not_installed`, `active`, `inactive` | 상태 필터 (해당 상태의 항목만 조회) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| include_hidden | query | boolean | 아니오 | — | manifest `hidden=true`로 숨김 처리된 확장까지 목록에 포함할지 여부 (기본 미포함) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.index_validation_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `sirsoft-admin_basic` | 템플릿 고유 식별자 (vendor-name 형식, 예: sirsoft-admin_basic) | +| vendor | string | `sirsoft` | 벤더/개발자명 (예: sirsoft) | +| name | string | `Admin Basic` | 템플릿 이름 (다국어 JSON) | +| version | string | `1.0.0` | 템플릿 버전 (예: 1.0.0) | +| type | string | `admin` | 템플릿 타입 (admin: 관리자용, user: 사용자용) | +| status | string | `active` | 상태 (active: 활성화, inactive: 비활성화, installing: 설치 중, uninstalling: 제거 중, updating: 업데이트 중) | +| description | string | `그누보드7 기본 관리자 템플릿` | 템플릿 설명 (다국어 JSON) | +| dependencies | object | `{"modules":[],"plugins":[]}` | 의존하는 확장 맵 (manifest 파생 — {modules, plugins}) | +| dependencies_met | boolean | `true` | 의존하는 모듈/플러그인이 모두 설치·활성 상태로 충족되었는지 여부 (activate 선행 검사 파생) | +| update_available | boolean | `false` | 최신 버전 대비 업데이트 가능 여부 | +| update_source | null | `null` | 업데이트 감지 출처 (github, bundled 등) | +| latest_version | string | `1.0.0` | 감지된 최신 배포 버전 | +| file_version | string | `1.0.0` | 설치된 파일의 manifest 버전 | +| github_url | string | `https://github.com/gnuboard/g7-templa…` | GitHub 저장소 URL | +| github_changelog_url | string | `https://github.com/gnuboard/g7-templa…` | GitHub 변경 내역 URL | +| is_pending | boolean | `false` | _pending 대기소에 있어 설치 대기 중인지 여부 | +| is_bundled | boolean | `false` | 코어에 선탑재된 번들 확장인지 여부 | +| deactivated_reason | null | `null` | 비활성화 사유: manual(사용자 수동) \| incompatible_core(코어 버전 호환성) \| null(active) | +| deactivated_at | null | `null` | deactivated 일시 | +| incompatible_required_version | null | `null` | 요구 코어 버전 미충족 시 필요한 버전 (호환되면 null) | +| abilities | object | `{"can_install":true,"can_activate":true,"can_uninstall":t…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 설치/미설치 템플릿을 모두 포함해 목록을 조회합니다(TemplateService::getPaginatedTemplates). `search`(이름·식별자·설명·벤더 OR 검색), `filters`(AND 조건), `status`, `type`, `include_hidden` 필터와 페이지네이션을 지원하며, 응답에는 현재 사용자의 install/activate/uninstall 수행 가능 여부(`abilities`)가 함께 담깁니다. `core.templates.read` 권한이 필요합니다. + + +### POST /api/admin/templates/activate + +- **라우트명**: `api.admin.templates.activate` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@activate` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.activate` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| template_name | body | string | 예 | max 255 | template 이름 (식별자) | +| force | body | boolean | 아니오 | — | 강제 실행 여부 (안전 확인/선행 검사 우회) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.activate_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.activate`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 설치된 템플릿을 활성화합니다. 필요한 의존 모듈/플러그인이 충족되지 않으면 409(warning)로 누락 목록을 반환하며, `force=true`로 강제 활성화할 수 있습니다. 활성화 성공 시 재활성화로 되살아나는 번들 언어팩 목록(`pending_language_packs`)을 함께 반환합니다. `core.templates.activate` 권한이 필요합니다. + + +### POST /api/admin/templates/check-updates + +- **라우트명**: `api.admin.templates.check-updates` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@checkUpdates` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.install` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | + + + +**설명** 설치된 모든 템플릿의 배포 최신 버전을 조회해 업데이트 가능 여부를 확인합니다(TemplateService::checkForUpdates). 별도 요청 파라미터는 없으며, 목록 화면의 업데이트 배지 갱신에 사용됩니다. `core.templates.install` 권한이 필요합니다. + + +### POST /api/admin/templates/deactivate + +- **라우트명**: `api.admin.templates.deactivate` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@deactivate` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.activate` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| template_name | body | string | 예 | max 255 | template 이름 (식별자) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.deactivate_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.activate`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 활성 상태의 템플릿을 비활성화합니다(TemplateService::deactivateTemplate). `core.templates.activate` 권한이 필요합니다. + + +### POST /api/admin/templates/install + +- **라우트명**: `api.admin.templates.install` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@install` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| template_name | body | string | 예 | max 255 | template 이름 (식별자) | +| dependencies | body | array | 아니오 | — | 함께 설치할 의존 확장 목록 (install-preview 응답 기반 사용자 선택분, 각 원소 type: module\|plugin, identifier) | +| language_packs | body | array | 아니오 | — | 함께 설치할 동반 번들 언어팩 식별자 배열 (설치 성공 후 best-effort 설치) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.install_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** `_bundled`/`_pending` 대기소의 템플릿을 활성 디렉토리로 설치합니다. 요청 시 함께 선택한 의존 확장을 먼저 설치(cascade 1단계)하고, 실패 시 중단합니다. 설치 성공 후 동반 번들 언어팩을 best-effort로 설치하며 실패 목록을 `language_pack_failures`로 반환합니다(cascade 2단계). `core.templates.install` 권한이 필요합니다. + + +### POST /api/admin/templates/install-from-file + +- **라우트명**: `api.admin.templates.install-from-file` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@installFromFile` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| file | body | file | 예 | max 51200 | 업로드 파일 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.install_from_file_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 업로드된 ZIP 파일에서 템플릿을 설치합니다(TemplateService::installFromZipFile). 최대 50MB(51200KB)까지 허용합니다. `core.templates.install` 권한이 필요합니다. + + +### POST /api/admin/templates/install-from-github + +- **라우트명**: `api.admin.templates.install-from-github` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@installFromGithub` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| github_url | body | string | 예 | — | GitHub 저장소 URL | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.install_from_github_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 지정한 GitHub 저장소 URL에서 템플릿을 내려받아 설치합니다(TemplateService::installFromGithub). `core.templates.install` 권한이 필요합니다. + + +### DELETE /api/admin/templates/layout-attachments/{attachment} + +- **라우트명**: `api.admin.templates.layout-attachments.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AdminTemplateLayoutAttachmentController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| attachment | path | string | 예 | — | 대상 attachment의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃 편집 중 업로드된 첨부(배경 이미지 등)를 삭제합니다. 스토리지의 실제 파일과 DB 행을 함께 삭제합니다(TemplateLayoutAttachmentService::delete). `core.templates.layouts.edit` 권한이 필요합니다. + + +### POST /api/admin/templates/manifest-preview + +- **라우트명**: `api.admin.templates.manifest-preview` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@manifestPreview` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| file | body | file | 예 | max 51200 | 업로드 파일 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 업로드된 ZIP을 실제 설치하지 않고 manifest와 검증 결과만 추출해 반환합니다(TemplateService::previewManifest). 설치 전 확인 다이얼로그에서 사용합니다. `core.templates.install` 권한이 필요합니다. + + +### POST /api/admin/templates/refresh-layouts + +- **라우트명**: `api.admin.templates.refresh-layouts` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@refreshLayouts` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.activate` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| template_name | body | string | 예 | max 255 | template 이름 (식별자) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.refresh_layouts_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.activate`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 템플릿의 레이아웃을 파일에서 다시 읽어 DB에 갱신합니다(TemplateService::refreshTemplateLayouts). 파일로 직접 수정한 레이아웃을 반영할 때 사용합니다. `core.templates.activate` 권한이 필요합니다. + + +### DELETE /api/admin/templates/uninstall + +- **라우트명**: `api.admin.templates.uninstall` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@uninstall` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.uninstall` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| template_name | query | string | 예 | max 255 | template 이름 (식별자) | +| delete_data | query | boolean | 아니오 | — | 제거 시 템플릿 관련 데이터(레이아웃/설정 등)까지 함께 삭제할지 여부 (기본 false = 파일만 제거) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.uninstall_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.uninstall`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 활성 디렉토리의 템플릿을 제거합니다(`_bundled` 원본은 보존). `delete_data=true`인 경우 관련 데이터까지 함께 삭제합니다(TemplateService::uninstallTemplate). `core.templates.uninstall` 권한이 필요합니다. + + +### GET /api/admin/templates/{identifier}/changelog + +- **라우트명**: `api.admin.templates.changelog` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@changelog` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| source | query | string | 아니오 | `active`, `bundled`, `github` | CHANGELOG 조회 출처 (active: 설치본, bundled: 코어 번들 원본, github: 원격 저장소) | +| from_version | query | string | 아니오 | — | 시작 버전 (범위 하한) | +| to_version | query | string | 아니오 | — | 대상 버전 (범위 상한) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.extension.changelog_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 템플릿의 변경 내역(CHANGELOG)을 조회합니다. `source`(active/bundled/github)로 출처를, `from_version`/`to_version`으로 버전 범위를 지정할 수 있습니다(TemplateService::getTemplateChangelog). `core.templates.read` 권한이 필요합니다. + + +### GET /api/admin/templates/{identifier}/editor-assets + +- **라우트명**: `api.admin.templates.editor-assets` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AdminTemplateAssetController@getEditorAssets` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃 편집기 부팅용 자산 매니페스트(컴포넌트 IIFE JS / CSS URL)를 반환합니다. 비활성 템플릿도 편집할 수 있도록 활성 디렉토리 → `_bundled` 폴백으로 빌드 산출물을 탐색하며, 빌드가 없으면 빈 목록으로 폴백합니다. `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{identifier}/editor/broadcast-catalog.json + +- **라우트명**: `api.admin.templates.editor-broadcast-catalog` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\BroadcastCatalogController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 데이터소스 websocket 후보용으로 등록된 브로드캐스트 채널/이벤트 카탈로그를 반환합니다(BroadcastCatalogService::collect). 편집기 전용 가드 하에서만 노출되며(admin 전역 broadcast 회피), `identifier`는 라우트 일관성용이고 카탈로그는 설치본 전역 기준입니다. `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{identifier}/editor/components.css + +- **라우트명**: `api.admin.templates.editor-css` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AdminTemplateAssetController@serveEditorCss` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃 편집기 프리뷰 전용 CSS를 서빙합니다. 편집기 진입 시에만 components.css의 다크 조상 셀렉터를 프리뷰 마커로 치환하고(editor-spec `darkMode.previewIsolation` 규칙), 필요 시 `@layer` 래퍼를 평탄화해 라이트/다크 프리뷰를 격리합니다. 변환 결과는 캐시 버전+파일 mtime 키로 캐시하며, CSS 부재 시 빈 응답으로 폴백합니다. `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{identifier}/editor/components.json + +- **라우트명**: `api.admin.templates.editor-components` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AdminTemplateAssetController@serveComponents` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 편집기용 components.json(컴포넌트 정의)을 서빙합니다. 비활성 템플릿도 편집 가능하도록 활성 디렉토리 → `_bundled` 폴백으로 파일을 읽어 반환합니다. `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{identifier}/editor/editor-spec.json + +- **라우트명**: `api.admin.templates.editor-spec` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AdminTemplateAssetController@serveEditorSpec` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 편집기 스펙(editor-spec.json)을 서빙합니다. 분할 스펙은 manifest + `$include` 블록을 합본한 단일 spec으로 반환하며(활성 디렉토리 기준), sampleData/sampleGlobal/states 등 전 블록이 포함됩니다. 파일 미존재 시 `spec=null`로 폴백합니다. `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{identifier}/editor/lang/{locale}.json + +- **라우트명**: `api.admin.templates.editor-lang` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AdminTemplateAssetController@serveLanguage` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| locale | path | string | 예 | — | 로케일 코드 (표시 언어/지역) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 편집기용 다국어 데이터(lang/{locale}.json)를 서빙합니다. 활성 상태 검증 없이 활성 디렉토리 → `_bundled` 폴백으로 읽으며, 파일 부재 시 빈 객체로 폴백합니다. `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{identifier}/editor/permission-candidates.json + +- **라우트명**: `api.admin.templates.editor-permission-candidates` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AdminTemplateAssetController@servePermissionCandidates` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃 편집기의 표시 권한 지정용으로 코어+활성 확장의 전체 권한을 `{key, name}` 목록으로 반환합니다(PermissionService::getPermissionCandidates). 편집기 진입 가드 하에서만 노출되어, 권한 카탈로그가 모든 admin 페이지에 상시 노출되던 방식보다 범위가 편집기로 한정됩니다. `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{identifier}/editor/routes.json + +- **라우트명**: `api.admin.templates.editor-routes` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AdminTemplateAssetController@serveRoutes` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 편집기 라우트 트리용 routes.json을 서빙합니다. 각 라우트에 `source`(kind/identifier) 태깅과 모듈/플러그인 라우트 병합을 적용해(TemplateService::getEditorRoutesDataWithModules) 클라이언트가 출처별로 그룹핑할 수 있게 하며, 활성/비활성 무관 + `_bundled` 폴백으로 동작합니다. `core.templates.layouts.edit` 권한이 필요합니다. + + +### POST /api/admin/templates/{identifier}/editor/seo-bot-preview + +- **라우트명**: `api.admin.templates.editor-seo-bot-preview` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SeoBotPreviewController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| layout | body | array | 예 | — | 편집 중인(dirty) 레이아웃 JSON 전체 (봇 HTML 렌더 대상) | +| route_params | body | array | 아니오 | — | 렌더 시 주입할 라우트 path 파라미터 맵 (예: 게시글 id 등 URL 동적 세그먼트) | +| url | body | string | 아니오 | — | 미리보기 기준 URL | +| locale | body | string | 아니오 | — | 로케일 코드 (표시 언어/지역) | +| module_id | body | string | 아니오 | — | module 식별자 | +| plugin_id | body | string | 아니오 | — | plugin 식별자 | +| seed_context | body | array | 아니오 | — | 렌더 컨텍스트 시드 데이터 (편집기 샘플 데이터 — 데이터소스/전역/로컬 값 대체) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.seo_bot_preview.show_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 편집기 [검색엔진] 탭의 봇 HTML 실시간 미리보기를 반환합니다. dirty 레이아웃 + 편집기 샘플 데이터로 운영과 동일한 렌더 경로를 거쳐(SEO 캐시 우회) 완성 HTML을 만들며, `meta.seo.enabled=false`이거나 미렌더 시 `enabled=false`로 미노출을 안내합니다. `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{identifier}/editor/seo-candidates.json + +- **라우트명**: `api.admin.templates.editor-seo-candidates` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SeoCandidateController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| extensions | query | string | 아니오 | — | 편집 중 레이아웃 문맥의 확장 목록 (JSON `[{type, id}]` 문자열 또는 배열 — SEO 후보 범위 결정용) | +| page_type | query | string | 아니오 | — | 편집 중 레이아웃의 페이지 유형 (page_type별 SEO 후보/토글 설정 필터) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.seo_candidate.index_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 편집기 [검색엔진] 탭의 SEO 후보(page_type/toggle_setting/유효 vars)를 한 번에 공급합니다(SeoCandidateService::collect). `extensions`(JSON `[{type,id}]`)와 `page_type` query로 편집 중 레이아웃 문맥을 전달하며, 후보 미존재 시 빈 목록 → 자유 텍스트 폴백입니다. `core.templates.layouts.edit` 권한이 필요합니다. + + +### POST /api/admin/templates/{identifier}/editor/seo-og-preview + +- **라우트명**: `api.admin.templates.editor-seo-og-preview` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\SeoOgPreviewController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| seo | body | array | 예 | — | 편집 중인(dirty) meta.seo 설정 전체 (base 병합본 — og/twitter cascade 계산 대상) | +| own_seo | body | array | 아니오 | — | 이 레이아웃이 직접 선언한 meta.seo (base 병합 전) — 병합본에만 있는 키를 base 상속으로 판정하는 근거 | +| seed_context | body | array | 아니오 | — | 렌더 컨텍스트 시드 데이터 (편집기 샘플 데이터 — 미리보기 값 계산용) | +| route_params | body | array | 아니오 | — | 렌더 시 주입할 라우트 path 파라미터 맵 (URL 동적 세그먼트) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.seo_og_preview.show_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 편집기 [검색엔진] 탭의 OG/Twitter/구조화 데이터 미리보기를 반환합니다. dirty `meta.seo` + 샘플로 og/twitter cascade를 실제 계산하고 필터 전/후 diff로 각 키의 출처·잠김을 산출합니다(SeoOgPreviewService::preview). `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{identifier}/layout-attachments + +- **라우트명**: `api.admin.templates.layout-attachments.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AdminTemplateLayoutAttachmentController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| layout_name | query | string | 아니오 | max 150 | layout 이름 (식별자) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template_layout_attachment.list_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 해당 템플릿의 레이아웃 첨부(배경 이미지 등) 목록을 조회합니다(ImagePickerControl의 이미지 재선택용). `layout_name` query로 특정 레이아웃의 첨부만 필터할 수 있습니다(TemplateLayoutAttachmentService::list). `core.templates.layouts.edit` 권한이 필요합니다. + + +### POST /api/admin/templates/{identifier}/layout-attachments + +- **라우트명**: `api.admin.templates.layout-attachments.store` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\AdminTemplateLayoutAttachmentController@store` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| file | body | file | 예 | max 10240 | 업로드 파일 | +| layout_name | body | string | 아니오 | max 150 | layout 이름 (식별자) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template_layout_attachment.upload_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃 편집 중 첨부 파일을 업로드합니다. 스토리지 저장 후 DB 행을 생성하고 접근 URL을 반환합니다(TemplateLayoutAttachmentService::upload). 최대 10MB(10240KB)까지 허용합니다. `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{identifier}/license + +- **라우트명**: `api.admin.templates.license` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@license` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 템플릿의 라이선스 파일(LICENSE) 내용을 반환합니다(LicenseService::getExtensionLicense). 식별자는 소문자/숫자/`_`/`-` 형식만 허용하며, 파일이 없으면 404를 반환합니다. `core.templates.read` 권한이 필요합니다. + + +### GET /api/admin/templates/{templateName} + +- **라우트명**: `api.admin.templates.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 템플릿의 상세 정보를 조회합니다(TemplateService::getTemplateInfo). 목록보다 상세한 필드(toDetailArray)와 함께 지원 언어팩 정보를 주입해 반환하며, 템플릿이 없으면 404를 반환합니다. `core.templates.read` 권한이 필요합니다. + + +### GET /api/admin/templates/{templateName}/check-modified-layouts + +- **라우트명**: `api.admin.templates.check-modified-layouts` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@checkModifiedLayouts` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 업데이트 전 사용자가 수정한 레이아웃이 있는지 확인합니다(TemplateService::checkModifiedLayouts). 업데이트 시 레이아웃 전략(overwrite/keep) 선택의 참고 자료로 사용합니다. `core.templates.read` 권한이 필요합니다. + + +### DELETE /api/admin/templates/{templateName}/custom-translations + +- **라우트명**: `api.admin.templates.custom-translations.bulk-destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateCustomTranslationController@bulkDestroy` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| ids | query | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.custom_translation.bulk_destroy_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리 모달의 '선택 삭제'/'미사용 전체 삭제'로 커스텀 다국어 키를 일괄 삭제합니다. 요청 `ids` 중 해당 템플릿 소속 키만 추려 삭제해(교차 템플릿 삭제 차단) 삭제 건수를 반환합니다. `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{templateName}/custom-translations + +- **라우트명**: `api.admin.templates.custom-translations.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateCustomTranslationController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| layout_name | query | string | 아니오 | max 150 | layout 이름 (식별자) | +| status | query | string | 아니오 | `active`, `orphaned` | 상태 필터 (해당 상태의 항목만 조회) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.custom_translation.index_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃 편집기의 인라인 편집/번역 탭에서 사용하는 커스텀 다국어 키 목록을 조회합니다. `layout_name`으로 특정 레이아웃을, `status`(active/orphaned)로 사용 여부를 필터할 수 있습니다(TemplateCustomTranslationService::getList). `core.templates.layouts.edit` 권한이 필요합니다. + + +### POST /api/admin/templates/{templateName}/custom-translations + +- **라우트명**: `api.admin.templates.custom-translations.store` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateCustomTranslationController@store` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| layout_name | body | string | 예 | max 150 | layout 이름 (식별자) | +| locale | body | string | 예 | max 35 | 로케일 코드 (표시 언어/지역) | +| value | body | string | 예 | — | 값 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.custom_translation.store_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 인라인 편집 확정 시 커스텀 다국어 키를 생성합니다. `layout_name`/`locale`/`value`를 받아 트랜잭션으로 키를 만들고 생성자를 기록합니다(TemplateCustomTranslationService::createKey). `core.templates.layouts.edit` 권한이 필요합니다. + + +### DELETE /api/admin/templates/{templateName}/custom-translations/{id} + +- **라우트명**: `api.admin.templates.custom-translations.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateCustomTranslationController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 커스텀 다국어 키 1건을 삭제합니다. 대상 키가 경로의 템플릿 소속인지 교차검증한 뒤 삭제합니다(TemplateCustomTranslationService::deleteKey). `core.templates.layouts.edit` 권한이 필요합니다. + + +### PUT /api/admin/templates/{templateName}/custom-translations/{id} + +- **라우트명**: `api.admin.templates.custom-translations.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateCustomTranslationController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| values | body | array | 예 | — | 값 배열 | +| expected_lock_version | body | integer | 예 | min 0 | 낙관적 잠금 버전 (동시 편집 충돌 감지) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.custom_translation.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 번역 탭의 일괄 편집으로 커스텀 다국어 키의 로케일별 값을 수정합니다. `expected_lock_version` 기반 낙관적 잠금을 적용해 동시 수정 충돌 시 409(current/your version)를 반환합니다(TemplateCustomTranslationService::updateValues). `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{templateName}/install-preview + +- **라우트명**: `api.admin.templates.install-preview` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@installPreview` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 템플릿 설치 cascade 프리뷰를 반환합니다. 의존 확장 목록과 동반 가능한 번들 언어팩을 미리 계산해(ExtensionInstallPreviewBuilder::build) 설치 확인 화면에서 사용합니다. `core.templates.install` 권한이 필요합니다. + + +### GET /api/admin/templates/{templateName}/layout-extensions + +- **라우트명**: `api.admin.templates.layout-extensions.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutExtensionController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 템플릿에 모듈/플러그인이 주입한 레이아웃 확장 목록을 출처별로 그룹핑해 조회합니다. 각 확장에는 호스트 레이아웃 목록(`host_layouts`)이 부착되어, 캔버스 로드 없이 화면별 연결 확장을 정적 구성할 수 있습니다(LayoutExtensionService::getExtensionsByTemplateId). `core.templates.read` 권한이 필요합니다. + + +### GET /api/admin/templates/{templateName}/layout-extensions/{extensionId} + +- **라우트명**: `api.admin.templates.layout-extensions.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutExtensionController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| extensionId | path | string | 예 | — | 대상 extension의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 레이아웃 확장 1건의 상세를 조회합니다. 확장이 경로의 템플릿 소속인지 교차검증하며, 편집 모드 캔버스가 호스트 병합 렌더에 쓰는 호스트 레이아웃 후보(`host_layouts`)를 함께 반환합니다. `core.templates.read` 권한이 필요합니다. + + +### PUT /api/admin/templates/{templateName}/layout-extensions/{extensionId} + +- **라우트명**: `api.admin.templates.layout-extensions.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutExtensionController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| extensionId | path | string | 예 | — | 대상 extension의 식별자 | +| expected_lock_version | body | integer | 예 | min 0 | 낙관적 잠금 버전 (동시 편집 충돌 감지) | +| content | body | array | 예 | — | 본문 내용 | +| priority | body | integer | 아니오 | min 0, max 9999 | 우선순위 (작을수록 우선) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.layout_extension.update_content_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃 확장의 content를 수정합니다. `expected_lock_version` 기반 낙관적 잠금으로 동시 수정 시 409를 반환하며, `content`는 최상위 키(extension_point/components 등) 누락을 막기 위해 검증된 하위 규칙이 아닌 원본 배열을 그대로 저장합니다. `priority`도 함께 갱신할 수 있습니다. `core.templates.layouts.edit` 권한이 필요합니다. + + +### POST /api/admin/templates/{templateName}/layout-extensions/{extensionId}/preview + +- **라우트명**: `api.admin.templates.layout-extensions.preview.store` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutExtensionController@storePreview` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| extensionId | path | string | 예 | — | 대상 extension의 식별자 | +| content | body | array | 예 | — | 본문 내용 | +| preview_layout | body | string | 아니오 | max 255 | 미리보기에 사용할 대표 레이아웃명 (extension_point 타입 시 필수, overlay 타입은 target_layout 자체가 대표라 생략 가능) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.layout_extension.store_preview_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 편집 중인 레이아웃 확장 content를 임시 저장하고 대표 레이아웃에 적용한 미리보기 URL/토큰을 반환합니다. overlay 타입은 target_name이, extension_point 타입은 `preview_layout` 파라미터가 대표 레이아웃이 되며, 미지정 시 422를 반환합니다(LayoutPreviewService::createExtensionPreview). `core.templates.read` 권한이 필요합니다. + + +### GET /api/admin/templates/{templateName}/layout-extensions/{extensionId}/versions + +- **라우트명**: `api.admin.templates.layout-extensions.versions.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutExtensionController@versions` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| extensionId | path | string | 예 | — | 대상 extension의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃 확장의 모든 버전 이력을 조회합니다(LayoutExtensionService::getExtensionVersions). `core.templates.read` 권한이 필요합니다. + + +### POST /api/admin/templates/{templateName}/layout-extensions/{extensionId}/versions/{versionId}/restore + +- **라우트명**: `api.admin.templates.layout-extensions.versions.restore` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutExtensionController@restoreVersion` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| extensionId | path | string | 예 | — | 대상 extension의 식별자 | +| versionId | path | string | 예 | — | 대상 version의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃 확장을 지정한 버전으로 복원합니다. 복원은 새 버전으로 기록되며 복원 후 새 버전 정보를 반환합니다(LayoutExtensionService::restoreExtensionVersion). `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{templateName}/layout-extensions/{extensionId}/versions/{version} + +- **라우트명**: `api.admin.templates.layout-extensions.versions.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutExtensionController@showVersion` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| extensionId | path | string | 예 | — | 대상 extension의 식별자 | +| version | path | string | 예 | — | 대상 버전 (버전 문자열) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃 확장의 특정 버전 content를 조회합니다(버전 비교/diff용, LayoutExtensionService::getExtensionVersion). `core.templates.read` 권한이 필요합니다. + + +### GET /api/admin/templates/{templateName}/layouts + +- **라우트명**: `api.admin.templates.layouts.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 템플릿의 모든 레이아웃 목록을 조회합니다. 각 레이아웃에 이름 → 라우트 path 매핑을 부착해, 코드 편집기가 파일 선택 시 `?route=` URL 동기화나 위지윅에서 넘어온 라우트로 파일을 복원할 수 있게 합니다(LayoutService::getLayoutsByTemplateId). `core.templates.read` 권한이 필요합니다. + + +### GET /api/admin/templates/{templateName}/layouts/{name} + +- **라우트명**: `api.admin.templates.layouts.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| name | path | string | 예 | — | 대상의 이름/명칭 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 레이아웃 1건의 상세(content 포함)를 조회합니다(LayoutService::getLayoutByName). 레이아웃이 없으면 404를 반환합니다. `core.templates.read` 권한이 필요합니다. + + +### PUT /api/admin/templates/{templateName}/layouts/{name} + +- **라우트명**: `api.admin.templates.layouts.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| name | path | string | 예 | — | 대상의 이름/명칭 | +| expected_lock_version | body | integer | 예 | min 0 | 낙관적 잠금 버전 (동시 편집 충돌 감지) | +| content | body | array | 예 | — | 본문 내용 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.layout.update_content_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃 content를 수정합니다. `expected_lock_version` 기반 낙관적 잠금을 적용해 동시 수정 충돌 시 409(current/your version)를 반환합니다(LayoutService::updateLayout). `core.templates.layouts.edit` 권한이 필요합니다. + + +### POST /api/admin/templates/{templateName}/layouts/{name}/preview + +- **라우트명**: `api.admin.templates.layouts.preview.store` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutController@storePreview` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| name | path | string | 예 | — | 대상의 이름/명칭 | +| content | body | array | 예 | — | 본문 내용 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 편집 중인 레이아웃 content를 임시 저장하고 미리보기 URL/토큰을 반환합니다(LayoutPreviewService::createPreview). 토큰은 만료 시각을 함께 반환합니다. `core.templates.read` 권한이 필요합니다. + + +### GET /api/admin/templates/{templateName}/layouts/{name}/versions + +- **라우트명**: `api.admin.templates.layouts.versions.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutController@versions` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| name | path | string | 예 | — | 대상의 이름/명칭 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃의 모든 버전 이력을 조회합니다(LayoutService::getLayoutVersions). `core.templates.read` 권한이 필요합니다. + + +### POST /api/admin/templates/{templateName}/layouts/{name}/versions/{versionId}/restore + +- **라우트명**: `api.admin.templates.layouts.versions.restore` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutController@restoreVersion` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.layouts.edit` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| name | path | string | 예 | — | 대상의 이름/명칭 | +| versionId | path | string | 예 | — | 대상 version의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃을 지정한 버전으로 복원합니다. 복원 결과는 새 버전으로 기록되어 반환됩니다(LayoutService::restoreVersion). `core.templates.layouts.edit` 권한이 필요합니다. + + +### GET /api/admin/templates/{templateName}/layouts/{name}/versions/{version} + +- **라우트명**: `api.admin.templates.layouts.versions.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\LayoutController@showVersion` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| name | path | string | 예 | — | 대상의 이름/명칭 | +| version | path | string | 예 | — | 대상 버전 (버전 문자열) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃의 특정 버전 content를 조회합니다. 버전 비교 diff용으로 slots/extends 등을 포함한 content 원본 전체를 노출합니다(LayoutService::getLayoutVersion). `core.templates.read` 권한이 필요합니다. + + +### GET /api/admin/templates/{templateName}/uninstall-info + +- **라우트명**: `api.admin.templates.uninstall-info` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@uninstallInfo` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.uninstall` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.uninstall`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 템플릿 제거 시 함께 삭제될 데이터 정보를 조회합니다(TemplateService::getTemplateUninstallInfo). 제거 확인 다이얼로그에서 사용하며, 템플릿이 없으면 404를 반환합니다. `core.templates.uninstall` 권한이 필요합니다. + + +### POST /api/admin/templates/{templateName}/update + +- **라우트명**: `api.admin.templates.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\TemplateController@performUpdate` +- **인증/권한**: `auth:sanctum` + `permission:core.templates.install` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| templateName | path | string | 예 | — | 대상 template의 이름 (식별자) | +| layout_strategy | body | string | 아니오 | `overwrite`, `keep` | 업데이트 시 레이아웃 처리 전략 (overwrite: 새 파일로 전면 교체, keep: 사용자 수정 레이아웃 유지) | +| force | body | boolean | 아니오 | — | 강제 실행 여부 (안전 확인/선행 검사 우회) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.template.perform_update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 설치된 템플릿을 최신 버전으로 업데이트합니다. `layout_strategy`(overwrite: 레이아웃 전면 교체 / keep: 사용자 수정 레이아웃 유지)로 레이아웃 처리 방식을 결정하며, `force`로 강제 진행할 수 있습니다(TemplateService::performVersionUpdate). `core.templates.install` 권한이 필요합니다. + + +### GET /api/templates/assets/{identifier}/{path} + +- **라우트명**: `api.public.templates.assets` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicTemplateController@serveAsset` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| path | path | string | 예 | — | 경로 | +| identifier | query | string | 예 | — | 대상 확장/리소스의 식별자 | +| path | query | string | 예 | — | 경로 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 템플릿의 정적 자산 파일(JS/CSS/이미지 등)을 서빙하는 공개 엔드포인트입니다. FormRequest에서 경로 보안 검증을 마친 뒤 파일을 조회하며(TemplateService::getAssetFilePath), ETag 및 장기 캐싱 헤더와 함께 반환합니다. 허용되지 않은 파일 유형은 403, 파일 부재 시 404입니다. 인증이 필요 없습니다. + + +### GET /api/templates/{identifier}/components.json + +- **라우트명**: `api.public.templates.components` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicTemplateController@serveComponents` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 템플릿의 컴포넌트 정의(components.json)를 서빙하는 공개 엔드포인트입니다(TemplateService::getComponentsFilePath). 프론트엔드 렌더 엔진 부팅에 사용하며 1시간 캐시됩니다. 인증이 필요 없습니다. + + +### GET /api/templates/{identifier}/config.json + +- **라우트명**: `api.public.templates.config` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicTemplateController@serveConfig` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 활성 템플릿의 설정 파일(template.json, error_config 등 메타데이터)을 서빙하는 공개 엔드포인트입니다. 응답에 확장 캐시 버전(`cache_version`)을 포함해 프론트엔드가 후속 API 호출에 사용하게 하며, 비활성/미존재 템플릿은 404입니다. 1시간 캐시됩니다. 인증이 필요 없습니다. + + +### GET /api/templates/{identifier}/editor-spec + +- **라우트명**: `api.public.templates.editor_spec` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicTemplateController@serveEditorSpec` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 템플릿의 편집기 스펙(editor-spec.json)을 서빙하는 공개 엔드포인트입니다. 분할 스펙은 manifest + `$include` 블록을 합본한 단일 spec으로(활성 디렉토리 기준) 반환하며, 파일 미존재 시 `spec=null`로 폴백합니다. 인증이 필요 없습니다. + + +### GET /api/templates/{identifier}/lang/{locale}.json + +- **라우트명**: `api.public.templates.language` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicTemplateController@serveLanguage` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| locale | path | string | 예 | — | 로케일 코드 (표시 언어/지역) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 활성 템플릿의 다국어 파일(lang/{locale}.json)을 활성화된 모듈의 다국어 데이터와 병합해 서빙하는 공개 엔드포인트입니다(TemplateService::getLanguageDataWithModules). 지원하지 않는 로케일/파일 부재 시 404이며 1시간 캐시됩니다. 인증이 필요 없습니다. + + +### GET /api/templates/{identifier}/layout-attachments/{attachment}/file + +- **라우트명**: `api.public.templates.layout-attachment-file` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicTemplateController@serveFile` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | +| attachment | path | string | 예 | — | 대상 attachment의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 레이아웃 첨부 이미지 파일을 서빙하는 공개 엔드포인트입니다. 발행된 배경 이미지가 일반 방문자에게도 로드되어야 하므로 인증 없이 노출하며, 비공개 attachments 디스크의 파일을 캐싱 헤더와 함께 인라인 스트림합니다. 첨부가 경로의 템플릿 소속이 아니거나 파일이 없으면 404입니다. 인증이 필요 없습니다. + + +### GET /api/templates/{identifier}/routes.json + +- **라우트명**: `api.public.templates.routes` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicTemplateController@getRoutes` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 템플릿의 라우트 정의(routes.json)를 활성화된 모듈의 라우트와 병합해 서빙하는 공개 엔드포인트입니다(TemplateService::getRoutesDataWithModules). 프론트엔드 라우팅 부팅에 사용하며 `v` query로 캐시를 무효화하고 1시간 캐시됩니다. 인증이 필요 없습니다. + + diff --git a/docs/backend/api/users.md b/docs/backend/api/users.md new file mode 100644 index 00000000..bdc92c87 --- /dev/null +++ b/docs/backend/api/users.md @@ -0,0 +1,607 @@ +# Users API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Users 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/admin/users + +- **라우트명**: `api.admin.users.index` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@index` +- **인증/권한**: `auth:sanctum` + `permission:core.users.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| filters | query | array | 아니오 | max 10 | 추가 필터 조건 맵 (필드별 조건) | +| start_date | query | date | 아니오 | — | 조회 기간 시작일 (이 날짜 이후 데이터) | +| end_date | query | date | 아니오 | — | 조회 기간 종료일 (이 날짜 이전 데이터) | +| date_filter | query | string | 아니오 | `all`, `week`, `month`, `custom` | 가입 기간 프리셋 (all: 전체, week: 최근 1주, month: 최근 1개월, custom: start_date/end_date 로 직접 지정) | +| sort_by | query | string | 아니오 | `created_at`, `name`, `email`, `last_login_at` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.list_validation_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `156` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| uuid | string | `a231747f-e82e-4cf2-9ae1-a261849dce40` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) | +| name | string | `API 문서 샘플 사용자` | 사용자 이름 | +| nickname | string | `gunwoo.oh` | 닉네임 | +| email | string | `apidoc-sample-user@example.com` | 이메일 주소 | +| language | string | `ko` | 사용자 언어 설정 (ko: 한국어, en: 영어) | +| language_label | string | `한국어` | 언어 코드의 현지화 라벨 (user.language.{code} 번역) | +| country | string | `KR` | 국가 코드 (ISO 3166-1 alpha-2) | +| country_flag | string | `🇰🇷` | 국가 코드의 국기 이모지 (country 값에서 파생) | +| country_name | string | `한국` | 국가 코드의 현지화 국가명 (country 값에서 파생) | +| status | string | `active` | 계정 상태 (active: 활성, inactive: 비활성, blocked: 차단, withdrawn: 탈퇴) | +| status_label | string | `활성` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| status_variant | string | `success` | 상태 표시 색상/스타일 변형 키 (상태 Enum variant() 산물 — UI 배지용) | +| mobile | string | `010-9070-5662` | 휴대폰 번호 | +| roles | array | `[{"id":1,"identifier":"admin","name":"관리자"}]` | 사용자에게 부여된 역할 목록 (원소: id/identifier/name — 역할 관계 파생) | +| email_verified_at | string | `2026-07-06 19:15:16` | email verified 일시 | +| last_login_at | string | `2026-07-05 19:15:16` | last login 일시 | +| created_at | string | `2026-07-06` | 생성 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_create":true,"can_update":true,"can…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +관리자 사용자 관리 화면(`admin_user_list.json`)의 목록을 제공합니다. `filters` 로 이름/이메일 다중 검색(operator: like, eq, starts_with, ends_with), `date_filter`(all/week/month/custom)와 `start_date`/`end_date` 로 가입 기간 필터, `sort_by`/`sort_order` 로 정렬한다. 응답은 `data.data[]`(항목별 순번 `number` + 요약 필드)와 `data.pagination`(페이지 정보)에 더해 `data.statistics`(통계)와 `data.abilities`(컬렉션 레벨 권한)를 함께 반환한다. 기본값은 `per_page=15`, `page=1`, `sort_by=created_at`, `sort_order=desc`, `date_filter=all` 이다. + + +### POST /api/admin/users + +- **라우트명**: `api.admin.users.store` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@store` +- **인증/권한**: `auth:sanctum` + `permission:core.users.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | string | 예 | max 255 | 대상의 이름/명칭 | +| nickname | body | string | 아니오 | max 50 | 닉네임 | +| email | body | email | 예 | max 255 | 이메일 주소 | +| password | body | string | 예 | — | 비밀번호 | +| language | body | string | 아니오 | `ko`, `en`, `fr`, `ja` | 언어 코드 | +| country | body | string | 아니오 | — | 국가 코드 (ISO 3166-1 alpha-2) | +| timezone | body | string | 아니오 | — | 타임존 식별자 | +| status | body | string | 아니오 | `active`, `inactive`, `blocked`, `withdrawn` | 계정 상태 (미지정 시 active) | +| homepage | body | string | 아니오 | max 255 | 홈페이지 URL | +| mobile | body | string | 아니오 | max 20 | 휴대전화 번호 | +| phone | body | string | 아니오 | max 20 | 전화번호 | +| zipcode | body | string | 아니오 | max 10 | 우편번호 | +| address | body | string | 아니오 | max 255 | 기본 주소 | +| address_detail | body | string | 아니오 | max 255 | 상세 주소 | +| signature | body | string | 아니오 | max 1000 | 서명 | +| bio | body | string | 아니오 | max 5000 | 자기소개 | +| admin_memo | body | string | 아니오 | max 5000 | 관리자 전용 메모 (해당 사용자에 대한 내부 기록, 사용자에게 노출 안 됨) | +| roles | body | array | 아니오 | min 1 | 부여할 역할 객체 배열 `[{id}]` (role_ids 와 병용 시 role_ids 우선) | +| role_ids | body | array | 아니오 | min 1 | role 식별자 배열 | +| notify_post_complete | body | boolean | 아니오 | — | 게시글 작성 완료 알림 수신 여부 (게시판 모듈 알림 설정) | +| notify_post_reply | body | boolean | 아니오 | — | 내 게시글에 답글이 달릴 때 알림 수신 여부 | +| notify_comment | body | boolean | 아니오 | — | 내 게시글에 댓글이 달릴 때 알림 수신 여부 | +| notify_reply_comment | body | boolean | 아니오 | — | 내 댓글에 대댓글이 달릴 때 알림 수신 여부 | +| email_subscription | body | boolean | 아니오 | — | 광고성 이메일 수신 동의 여부 (marketing 플러그인 채널 동의, marketing_consents 테이블에 저장) | +| marketing_consent | body | boolean | 아니오 | — | 마케팅 정보 수신 전체 동의 마스터 키 (marketing 플러그인이 검증 규칙 주입, 상세는 user-consent-injection.md) | +| third_party_consent | body | boolean | 아니오 | — | 제3자 정보 제공 동의 여부 (marketing 플러그인 법적 동의 항목) | +| info_disclosure | body | boolean | 아니오 | — | 개인정보 이용 안내 동의 여부 (marketing 플러그인 법적 동의 항목) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.create_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.users.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +관리자가 새 사용자를 생성합니다. `name`, `email`, `password`(8자 이상, `password_confirmation` 확인 필수), 그리고 역할이 필수이다 — 역할은 `roles`(객체 배열 `[{id}]`) 또는 `role_ids`(id 배열) 중 하나로 지정하며 둘 다 보내면 `role_ids` 가 우선한다. `language`(미지정 시 `ko`), `status`(미지정 시 `active`), 연락처/주소/자기소개 등은 선택 항목이다. 성공 시 201 과 함께 생성된 사용자를 `UserResource` 형태로 반환한다. `notify_*`/`marketing_consent`/`third_party_consent`/`info_disclosure`/`email_subscription` 파라미터는 확장(sirsoft-marketing)이 검증 규칙을 주입한 필드로, 상세는 해당 확장 문서를 참조한다. + + +### PATCH /api/admin/users/bulk-status + +- **라우트명**: `api.admin.users.bulk-status` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@bulkUpdateStatus` +- **인증/권한**: `auth:sanctum` + `permission:core.users.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| status | body | string | 예 | — | 일괄 적용할 계정 상태 (UserStatus Enum 값: active/inactive/blocked/withdrawn/pending_verification) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.bulk_update_status_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.users.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +여러 사용자의 계정 상태를 한 번에 변경합니다. `ids` 는 대상 사용자 UUID 배열이며, `status` 는 `active`/`inactive`/`blocked`/`withdrawn`/`pending_verification`(UserStatus Enum 값) 중 하나이다. `ExcludeCurrentUser` 규칙으로 요청자 본인은 대상에서 제외된다. 목록 화면의 다중 선택 후 일괄 상태 변경에 사용한다. + + +### POST /api/admin/users/check-email + +- **라우트명**: `api.admin.users.check-email` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@checkEmail` +- **인증/권한**: `auth:sanctum` + `permission:core.users.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| email | body | email | 예 | max 255 | 이메일 주소 | +| exclude_user_id | body | uuid | 아니오 | — | exclude user 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.check_email_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +이메일 주소의 사용 가능 여부(중복 아님)를 확인합니다. `email` 은 필수, `exclude_user_id`(UUID)를 주면 해당 사용자를 중복 검사에서 제외한다 — 사용자 수정 화면에서 자기 자신의 이메일을 유지할 때 사용한다. 응답 `data.available` 이 true 면 사용 가능, false 면 이미 사용 중이다. 사용자 생성/수정 폼의 이메일 실시간 중복 확인에 쓰인다. + + +### PATCH /api/admin/users/me/language + +- **라우트명**: `api.admin.users.me.language` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@updateMyLanguage` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| language | body | string | 예 | `ko`, `en`, `fr`, `ja` | 언어 코드 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.update_language_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +현재 로그인한 사용자 본인의 언어 설정을 변경합니다. 별도 권한 없이 `auth:sanctum` 인증만 요구하며(다른 사용자를 대상으로 하지 않음), `language` 는 `config('app.supported_locales')`(예: ko, en, fr, ja) 중 하나여야 한다. 성공 시 갱신된 사용자를 `UserResource` 로 반환하며, 관리자 UI 의 언어 전환에 사용한다. + + +### GET /api/admin/users/recent + +- **라우트명**: `api.admin.users.recent` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@recent` +- **인증/권한**: `auth:sanctum` + `permission:core.users.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| uuid | string | `a231747f-e82e-4cf2-9ae1-a261849dce40` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) | +| name | string | `API 문서 샘플 사용자` | 사용자 이름 | +| nickname | string | `gunwoo.oh` | 닉네임 | +| 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` | 계정 상태 (active: 활성, inactive: 비활성, blocked: 차단, withdrawn: 탈퇴) | +| 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-9070-5662` | 휴대폰 번호 | +| phone | string | `02-805-4759` | 전화번호 | +| zipcode | string | `93153` | 우편번호 | +| address | string | `대구광역시 북구 백제고분로 720` | 기본 주소 | +| address_detail | string | `40동 835호` | 상세 주소 | +| signature | string | `Ipsam rem amet expedita est.` | 서명 | +| bio | string | `Tenetur omnis et amet omnis veniam to…` | 자기소개 | +| last_login_at | string | `2026-07-05 19:15:16` | last login 일시 | +| email_verified_at | string | `2026-07-06 19:15:16` | email verified 일시 | +| timezone | string | `Asia/Seoul` | 사용자 시간대 (예: Asia/Seoul, UTC) | +| created_at | string | `2026-07-06 19:15:16` | 생성 일시 | +| updated_at | string | `2026-07-06 19:15:16` | 최종 수정 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_create":true,"can_update":true,"can…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 | + + + +**설명** + +최근 가입한 사용자 10명을 최신순으로 반환합니다. 파라미터는 없으며, `UserResource` 전체 필드(관계형 데이터는 미로드)를 담은 컬렉션을 반환한다. 관리자 대시보드의 최근 가입자 위젯이 소비한다. + + +### GET /api/admin/users/search + +- **라우트명**: `api.admin.users.search` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@search` +- **인증/권한**: `auth:sanctum` + `permission:core.users.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| keyword | query | string | 아니오 | max 255 | 검색 키워드 (부분 일치) | +| uuid | query | uuid | 아니오 | — | 특정 사용자 UUID 로 단건 조회 (지정 시 keyword 무시하고 해당 UUID 사용자만 반환) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.search_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +사용자를 검색해 `UserResource` 컬렉션으로 반환합니다. `uuid` 를 주면 해당 UUID 의 단일 사용자(존재 시 1건, 없으면 빈 배열)를, 없으면 `keyword` 로 이름·닉네임·이메일을 부분 일치 검색한다(`keyword` 와 `uuid` 중 하나는 필수). 알림 템플릿 수신자 지정이나 활동 로그 필터의 사용자 선택 UI 에서 사용한다. + + +### GET /api/admin/users/statistics + +- **라우트명**: `api.admin.users.statistics` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@statistics` +- **인증/권한**: `auth:sanctum` + `permission:core.users.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| total_users | integer | `156` | 전체 사용자 수 (통계 객체는 count/추이 포함) | +| users_this_week | integer | `1` | 이번 주 신규 가입자 수 | +| users_this_month | integer | `155` | 이번 달 신규 가입자 수 | +| users_today | integer | `0` | 오늘 신규 가입자 수 | +| active_users_this_week | integer | `2` | 이번 주 활동(로그인) 사용자 수 | +| language_distribution | object | `{"ko":92,"en":64}` | 언어별 사용자 분포 (언어 코드 => 사용자 수) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 | + + + +**설명** 관리자 대시보드의 사용자 통계 위젯이 소비하는 집계 API. 캐시 없이 실시간 집계. + + +### DELETE /api/admin/users/{user} + +- **라우트명**: `api.admin.users.destroy` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:core.users.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| user | path | string | 예 | — | 대상 user의 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.delete_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.users.delete`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +지정한 사용자(경로 파라미터는 UUID 로 바인딩)를 삭제합니다. 슈퍼관리자 계정은 삭제할 수 없으며 시도 시 422(`exceptions.cannot_delete_super_admin`)를 반환한다. 그 외 삭제 실패 시에는 실패 상세 사유가 담긴 422 를, 나머지 오류는 500 을 반환한다. 삭제는 Service 계층에서 관련 데이터 정리와 훅을 거쳐 처리된다. + + +### GET /api/admin/users/{user} + +- **라우트명**: `api.admin.users.show` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@show` +- **인증/권한**: `auth:sanctum` + `permission:core.users.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| user | path | string | 예 | — | 대상 user의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| uuid | string | `a231747f-e82e-4cf2-9ae1-a261849dce40` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) | +| name | string | `API 문서 샘플 사용자` | 사용자 이름 | +| nickname | string | `gunwoo.oh` | 닉네임 | +| 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` | 계정 상태 (active: 활성, inactive: 비활성, blocked: 차단, withdrawn: 탈퇴) | +| 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-9070-5662` | 휴대폰 번호 | +| phone | string | `02-805-4759` | 전화번호 | +| zipcode | string | `93153` | 우편번호 | +| address | string | `대구광역시 북구 백제고분로 720` | 기본 주소 | +| address_detail | string | `40동 835호` | 상세 주소 | +| signature | string | `Ipsam rem amet expedita est.` | 서명 | +| bio | string | `Tenetur omnis et amet omnis veniam to…` | 자기소개 | +| last_login_at | string | `2026-07-05 19:15:16` | last login 일시 | +| email_verified_at | string | `2026-07-06 19:15:16` | email verified 일시 | +| timezone | string | `Asia/Seoul` | 사용자 시간대 (예: Asia/Seoul, UTC) | +| modules_count | integer | `0` | modules 개수 (집계) | +| plugins_count | integer | `0` | plugins 개수 (집계) | +| menus_count | integer | `2` | menus 개수 (집계) | +| modules | array | `[]` | 이 사용자가 접근 권한을 가진 모듈 목록 (역할 경유 권한 관계 파생) | +| plugins | array | `[]` | 이 사용자가 접근 권한을 가진 플러그인 목록 (역할 경유 권한 관계 파생) | +| menus | array | `[{"id":33,"title":"API 문서 샘플 메뉴","url":"\/admin\/apidoc-s…` | 이 사용자가 접근 가능한 관리자 메뉴 목록 (원소: id/title/url — 역할 경유 메뉴 관계 파생) | +| roles | array | `[{"id":1,"identifier":"admin","name":"관리자"}]` | 사용자에게 부여된 역할 목록 (원소: id/identifier/name — 역할 관계 파생) | +| permissions | array | `[]` | 연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) | +| consents | array | `[]` | 사용자가 동의한 약관 동의 레코드 목록 (약관 관계 파생) | +| terms_consent | null | `null` | 이용약관 동의 정보 (동의 시각 등, 미동의 시 null) | +| privacy_consent | null | `null` | 개인정보 처리방침 동의 정보 (동의 시각 등, 미동의 시 null) | +| created_at | string | `2026-07-06 19:15:16` | 생성 일시 | +| updated_at | string | `2026-07-06 19:15:16` | 최종 수정 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_create":true,"can_update":true,"can…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +| admin_memo | string | `Perferendis ut suscipit quia unde sed.` | 관리자 메모 | +| ip_address | string | `77.105.222.87` | 마지막 접속 IP 주소 | +| withdrawn_at | null | `null` | withdrawn 일시 | +| blocked_at | null | `null` | blocked 일시 | +| notify_post_complete | boolean | `false` | 게시글 작성 완료 알림 수신 여부 (게시판 모듈 알림 설정) | +| notify_post_reply | boolean | `false` | 내 게시글 답글 알림 수신 여부 | +| notify_comment | boolean | `false` | 내 게시글 댓글 알림 수신 여부 | +| notify_reply_comment | boolean | `false` | 내 댓글 대댓글 알림 수신 여부 | +| email_subscription | boolean | `false` | 광고성 이메일 수신 동의 여부 (marketing 플러그인 채널 동의) | +| email_subscription_at | null | `null` | email subscription 일시 | +| marketing_consent | boolean | `false` | 마케팅 정보 수신 전체 동의 여부 (marketing 플러그인 마스터 키, 미동의 시 false) | +| marketing_consent_at | null | `null` | marketing consent 일시 | +| third_party_consent | boolean | `false` | 제3자 정보 제공 동의 여부 (marketing 플러그인 법적 동의 항목) | +| third_party_consent_at | null | `null` | third party consent 일시 | +| info_disclosure | boolean | `false` | 개인정보 이용 안내 동의 여부 (marketing 플러그인 법적 동의 항목) | +| info_disclosure_at | null | `null` | info disclosure 일시 | +| marketing_consent_enabled | boolean | `true` | 마케팅 동의 UI 노출 여부 (marketing 플러그인 활성화 플래그, 기본 true) | +| marketing_consent_terms_slug | string | `marketing-terms` | 마케팅 약관 slug (연결된 약관 페이지 식별자, 미설정 시 null) | +| marketing_consent_terms_slug_set | boolean | `true` | 마케팅 약관 연결 존재 여부 (프론트 약관 링크 표시 판정용) | +| third_party_consent_enabled | boolean | `true` | 제3자 제공 동의 항목 노출 여부 (marketing 플러그인 활성화 플래그) | +| third_party_consent_terms_slug | null | `null` | 제3자 제공 약관 slug (미설정 시 null) | +| third_party_consent_terms_slug_set | boolean | `false` | 제3자 제공 약관 연결 존재 여부 | +| info_disclosure_enabled | boolean | `true` | 정보 이용 안내 동의 항목 노출 여부 (marketing 플러그인 활성화 플래그) | +| info_disclosure_terms_slug | null | `null` | 정보 이용 안내 약관 slug (미설정 시 null) | +| info_disclosure_terms_slug_set | boolean | `false` | 정보 이용 안내 약관 연결 존재 여부 | +| email_subscription_enabled | boolean | `true` | 이메일 수신 채널 노출 여부 (marketing 플러그인 활성화 플래그) | +| email_subscription_terms_slug | null | `null` | 이메일 수신 약관 slug (미설정 시 null) | +| email_subscription_terms_slug_set | boolean | `false` | 이메일 수신 약관 연결 존재 여부 | +| channels | array | `[{"key":"email_subscription","label":"광고성 이메일 수신","enable…` | 관리자 정의 전체 마케팅 채널 목록 (원소: key/label/enabled/terms_slug — marketing 플러그인 주입, iteration 렌더링용) | +| consent_histories | array | `[]` | 사용자 동의 변경 이력 (원소: channel_key/action/source/created_at — marketing 플러그인 주입) | +| ecommerce_mileage | object | `{"enabled":false}` | 이커머스 마일리지 정보 (enabled 및 잔액 등 — sirsoft-ecommerce 모듈 주입) | +| ecommerce_preferred_currency | null | `null` | 선호 결제 통화 (sirsoft-ecommerce 모듈 주입, 미설정 시 null) | +| ecommerce_preferred_shipping_country | null | `null` | 선호 배송 국가 코드 (sirsoft-ecommerce 모듈 주입, 미설정 시 null) | +| ecommerce_preferred_shipping_country_name | null | `null` | 선호 배송 국가명 (배송 국가 코드에서 파생, sirsoft-ecommerce 모듈 주입) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +특정 사용자의 상세 정보를 조회합니다(경로 파라미터는 UUID 로 바인딩). `withAdminInfo()` 를 통해 기본 필드에 더해 관리자 전용 필드(admin_memo, ip_address, withdrawn_at, blocked_at)와 관계형 데이터(modules, plugins, menus, roles, permissions, consents 및 개수 필드)를 함께 반환한다. `core.user.filter_resource_data` 필터로 확장이 자신의 필드(sirsoft-marketing 의 알림/동의 설정, sirsoft-ecommerce 의 마일리지/선호 통화·배송국 등)를 병합한다. 관리자 사용자 상세/수정 화면(`admin_user_detail.json`/`admin_user_form.json`)이 소비한다. + + +### PUT /api/admin/users/{user} + +- **라우트명**: `api.admin.users.update` +- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@update` +- **인증/권한**: `auth:sanctum` + `permission:core.users.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| user | path | string | 예 | — | 대상 user의 식별자 | +| name | body | string | 예 | max 255 | 대상의 이름/명칭 | +| nickname | body | string | 아니오 | max 50 | 닉네임 | +| email | body | email | 예 | max 255 | 이메일 주소 | +| password | body | string | 아니오 | — | 비밀번호 | +| language | body | string | 아니오 | `ko`, `en`, `fr`, `ja` | 언어 코드 | +| country | body | string | 아니오 | — | 국가 코드 (ISO 3166-1 alpha-2) | +| timezone | body | string | 아니오 | — | 타임존 식별자 | +| status | body | string | 아니오 | `active`, `inactive`, `blocked`, `withdrawn` | 계정 상태 (미지정 시 active) | +| homepage | body | string | 아니오 | max 255 | 홈페이지 URL | +| mobile | body | string | 아니오 | max 20 | 휴대전화 번호 | +| phone | body | string | 아니오 | max 20 | 전화번호 | +| zipcode | body | string | 아니오 | max 10 | 우편번호 | +| address | body | string | 아니오 | max 255 | 기본 주소 | +| address_detail | body | string | 아니오 | max 255 | 상세 주소 | +| signature | body | string | 아니오 | max 1000 | 서명 | +| bio | body | string | 아니오 | max 5000 | 자기소개 | +| admin_memo | body | string | 아니오 | max 5000 | 관리자 전용 메모 (해당 사용자에 대한 내부 기록, 사용자에게 노출 안 됨) | +| roles | body | array | 아니오 | min 1 | 부여할 역할 객체 배열 `[{id}]` (role_ids 와 병용 시 role_ids 우선) | +| role_ids | body | array | 아니오 | min 1 | role 식별자 배열 | +| notify_post_complete | body | boolean | 아니오 | — | 게시글 작성 완료 알림 수신 여부 (게시판 모듈 알림 설정) | +| notify_post_reply | body | boolean | 아니오 | — | 내 게시글에 답글이 달릴 때 알림 수신 여부 | +| notify_comment | body | boolean | 아니오 | — | 내 게시글에 댓글이 달릴 때 알림 수신 여부 | +| notify_reply_comment | body | boolean | 아니오 | — | 내 댓글에 대댓글이 달릴 때 알림 수신 여부 | +| email_subscription | body | boolean | 아니오 | — | 광고성 이메일 수신 동의 여부 (marketing 플러그인 채널 동의, marketing_consents 테이블에 저장) | +| marketing_consent | body | boolean | 아니오 | — | 마케팅 정보 수신 전체 동의 마스터 키 (marketing 플러그인이 검증 규칙 주입, 상세는 user-consent-injection.md) | +| third_party_consent | body | boolean | 아니오 | — | 제3자 정보 제공 동의 여부 (marketing 플러그인 법적 동의 항목) | +| info_disclosure | body | boolean | 아니오 | — | 개인정보 이용 안내 동의 여부 (marketing 플러그인 법적 동의 항목) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`core.users.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +기존 사용자 정보를 수정합니다(경로 파라미터는 UUID 로 바인딩). `name`, `email` 은 필수이며 `email` 은 해당 사용자를 제외한 고유성 검사를 거친다. `password` 는 선택이며 값을 주면 8자 이상·`password_confirmation` 확인을 요구한다(미전송 시 기존 비밀번호 유지). 역할은 `roles` 또는 `role_ids` 중 하나로 지정하고 둘 다 오면 `role_ids` 가 우선한다. 성공 시 갱신된 사용자를 `UserResource` 로 반환한다. `notify_*`/`marketing_consent`/`third_party_consent`/`info_disclosure`/`email_subscription` 파라미터는 확장(sirsoft-marketing)이 검증 규칙을 주입한 필드로, 상세는 해당 확장 문서를 참조한다. + + +### GET /api/users/{user}/profile + +- **라우트명**: `api.public.users.profile` +- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicProfileController@show` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| user | path | string | 예 | — | 대상 user의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| uuid | string | `a231747f-e82e-4cf2-9ae1-a261849dce40` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) | +| name | string | `API 문서 샘플 사용자` | 사용자 이름 | +| status | string | `active` | 계정 상태 (active: 활성, inactive: 비활성, blocked: 차단, withdrawn: 탈퇴) | +| status_label | string | `활성` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| avatar | null | `null` | 프로필 아바타 이미지 URL (미설정 시 null) | +| bio | string | `Tenetur omnis et amet omnis veniam to…` | 자기소개 | +| created_at | string | `2026-07-06` | 생성 일시 | +| is_withdrawn | boolean | `false` | withdrawn 여부 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +타인의 공개 프로필을 인증 없이 조회합니다(경로 파라미터는 UUID 로 바인딩, `users/show.json` 이 소비). 사용자 상태에 따라 노출 필드가 달라진다 — active 는 name/avatar/bio/created_at 전체, inactive 는 bio 를 제외, blocked 는 avatar/bio/created_at 를 모두 제외한다. withdrawn 사용자는 이름을 익명 표기로 대체하고 `is_withdrawn=true` 로 반환하며, 미존재 사용자는 404(`user.not_found`)를 반환한다. 게시글 통계는 게시판 모듈 API 로 별도 조회한다. + + diff --git a/docs/backend/api/verify-password.md b/docs/backend/api/verify-password.md new file mode 100644 index 00000000..35a88b04 --- /dev/null +++ b/docs/backend/api/verify-password.md @@ -0,0 +1,51 @@ +# Verify Password API 레퍼런스 + +> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Verify Password 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### POST /api/me/verify-password + +- **라우트명**: `api.me.verify-password` +- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@verifyPassword` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| password | body | string | 예 | — | 비밀번호 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.verify_password_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +현재 로그인한 사용자의 비밀번호를 재확인한다. 민감한 작업 직전 본인 확인 게이트로 사용되며, 프론트의 `_password_verify_section.json` 이 호출한다. 요청의 `password` 를 `Hash::check` 로 저장된 해시와 대조해, 일치하면 성공 응답(`user.password_verified`)을, 틀리면 401(`user.password_incorrect`)을 반환한다. 비밀번호를 변경하지 않고 신원만 확인하므로 사용자 데이터는 바뀌지 않는다. + + diff --git a/docs/backend/routing.md b/docs/backend/routing.md index b1274e8c..b54825ac 100644 --- a/docs/backend/routing.md +++ b/docs/backend/routing.md @@ -68,7 +68,8 @@ | 코어 | `/admin/[기능명]` | `/admin/users` | | 모듈 | `/admin/[vendor-module]/[기능명]` | `/admin/sirsoft-ecommerce/products` | | 플러그인 | `/admin/[vendor-plugin]/[기능명]` | `/admin/sirsoft-payment/settings` | -| 공개 API | `/api/[vendor-module]/[기능명]` | `/api/sirsoft-ecommerce/products` | +| 모듈 공개 API | `/api/modules/[vendor-module]/[기능명]` | `/api/modules/sirsoft-ecommerce/products` | +| 플러그인 공개 API | `/api/plugins/[vendor-plugin]/[기능명]` | `/api/plugins/sirsoft-gdpr/consent` | ### 리소스 URL 규칙 @@ -149,32 +150,35 @@ sirsoft-ecommerce.products.delete ### 모듈 라우트 파일 ```php -// modules/sirsoft-ecommerce/src/routes/api.php +// modules/_bundled/sirsoft-ecommerce/src/routes/api.php use Illuminate\Support\Facades\Route; use Modules\Sirsoft\Ecommerce\Controllers\Api\Admin\ProductController; -Route::prefix('admin/sirsoft-ecommerce')->middleware(['auth:sanctum', 'admin'])->group(function () { - // 상품 관리 (권한 체크 포함) +// ModuleRouteServiceProvider 가 URL prefix('api/modules/sirsoft-ecommerce')와 +// name prefix('api.modules.sirsoft-ecommerce.')를 자동 적용한다. +// 라우트 파일 내부 group 에는 관리자 세그먼트('admin')만 두고, 접두는 중복 입력하지 않는다. +Route::prefix('admin')->middleware(['auth:sanctum', 'admin'])->group(function () { + // 상품 관리 (권한 체크 포함) → 최종 URL: /api/modules/sirsoft-ecommerce/admin/products Route::get('/products', [ProductController::class, 'index']) ->middleware('permission:sirsoft-ecommerce.products.view') - ->name('api.sirsoft-ecommerce.products.index'); + ->name('products.index'); // 최종 name: api.modules.sirsoft-ecommerce.products.index Route::post('/products', [ProductController::class, 'store']) ->middleware('permission:sirsoft-ecommerce.products.create') - ->name('api.sirsoft-ecommerce.products.store'); + ->name('products.store'); Route::get('/products/{id}', [ProductController::class, 'show']) ->middleware('permission:sirsoft-ecommerce.products.view') - ->name('api.sirsoft-ecommerce.products.show'); + ->name('products.show'); Route::put('/products/{id}', [ProductController::class, 'update']) ->middleware('permission:sirsoft-ecommerce.products.edit') - ->name('api.sirsoft-ecommerce.products.update'); + ->name('products.update'); Route::delete('/products/{id}', [ProductController::class, 'destroy']) ->middleware('permission:sirsoft-ecommerce.products.delete') - ->name('api.sirsoft-ecommerce.products.destroy'); + ->name('products.destroy'); }); ``` @@ -253,17 +257,20 @@ Route::middleware('auth:sanctum')->prefix('user')->group(function () { ### 공개 API 라우트 ```php -// modules/sirsoft-ecommerce/src/routes/api.php +// modules/_bundled/sirsoft-ecommerce/src/routes/api.php use Modules\Sirsoft\Ecommerce\Controllers\Api\Public\ProductController; -Route::prefix('api/sirsoft-ecommerce')->group(function () { - // 공개 상품 API (인증 불필요) - Route::get('/products', [ProductController::class, 'index']) - ->name('api.sirsoft-ecommerce.public.products.index'); +// ModuleRouteServiceProvider 가 URL prefix('api/modules/sirsoft-ecommerce')와 +// name prefix('api.modules.sirsoft-ecommerce.')를 자동 적용한다. +// 라우트 파일에서 prefix/name 접두를 중복 입력하지 않는다. +Route::prefix('products')->group(function () { + // 공개 상품 API (인증 불필요) → 최종 URL: /api/modules/sirsoft-ecommerce/products + Route::get('/', [ProductController::class, 'index']) + ->name('public.products.index'); // 최종 name: api.modules.sirsoft-ecommerce.public.products.index - Route::get('/products/{id}', [ProductController::class, 'show']) - ->name('api.sirsoft-ecommerce.public.products.show'); + Route::get('/{id}', [ProductController::class, 'show']) + ->name('public.products.show'); }); ``` diff --git a/modules/_bundled/gnuboard7-hello_module/docs/api/memos.md b/modules/_bundled/gnuboard7-hello_module/docs/api/memos.md new file mode 100644 index 00000000..821b1e66 --- /dev/null +++ b/modules/_bundled/gnuboard7-hello_module/docs/api/memos.md @@ -0,0 +1,117 @@ +# Memos API 레퍼런스 + +> **소유**: module `gnuboard7-hello_module` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Memos 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/gnuboard7-hello_module/memos + +- **라우트명**: `api.modules.gnuboard7-hello_module.memos.index` +- **컨트롤러**: `Modules\Gnuboard7\HelloModule\Http\Controllers\Api\MemoController@index` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** + +메모 목록을 페이지네이션으로 조회하는 공개 엔드포인트다. 라우트는 `optional.sanctum` 미들웨어를 쓰므로 **비로그인 사용자도 조회할 수 있다**(생성기 표기 `auth:sanctum` 은 실제와 다르며, 실제로는 토큰이 있으면 인증 컨텍스트를 붙이되 없어도 통과한다). 이 확장은 학습용 샘플로, 공개 읽기 API 의 표준 패턴(`PublicBaseController` + `throttle:600,1`)을 보여준다. + +- **요청 파라미터**: query `per_page`(정수, 기본 10)로 페이지 크기를 조절한다. FormRequest 를 쓰지 않고 컨트롤러가 `$request->query('per_page', 10)` 로 직접 읽는다. +- **응답**: `data` 는 `MemoCollection`(`BaseApiCollection`) 산물로, `data.data` 에 `MemoResource` 배열, `data.meta.pagination` 에 페이지 메타(`current_page`/`last_page`/`per_page`/`total`/`from`/`to`/`has_more_pages`)를 담는다. 각 메모 항목 필드는 아래 상세 조회와 동일하다. +- **미설치 주의**: 이 문서는 확장이 미설치인 상태에서 라우트 파일 정적 분석으로 생성되어 실측 응답이 없다(`http-404`). 설치 후 `api:docgen --scope=module:gnuboard7-hello_module --seed` 로 실측하면 응답 예시가 채워진다. + +**응답 예시** (정적 — MemoResource 구조 기준) + +```json +{ + "success": true, + "data": { + "data": [ + { "id": 1, "uuid": "…", "title": "샘플 메모", "content": "본문", "created_at": "…", "updated_at": "…", "is_owner": false, "abilities": { "can_create": false, "can_update": false, "can_delete": false } } + ], + "meta": { "pagination": { "current_page": 1, "last_page": 1, "per_page": 10, "total": 1, "from": 1, "to": 1, "has_more_pages": false } } + }, + "message": "메모를 조회했습니다.", + "error": null +} +``` + + +### GET /api/modules/gnuboard7-hello_module/memos/{id} + +- **라우트명**: `api.modules.gnuboard7-hello_module.memos.show` +- **컨트롤러**: `Modules\Gnuboard7\HelloModule\Http\Controllers\Api\MemoController@show` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +단일 메모를 조회하는 공개 엔드포인트다. 목록과 마찬가지로 `optional.sanctum` 이라 비로그인 조회가 허용된다(생성기 표기 `auth:sanctum` 은 실제와 다름). + +- **path 파라미터** `{id}`: 메모의 정수 PK. 라우트 제약 `whereNumber('id')` 로 숫자만 매칭된다. 라우트-모델 바인딩 없이 컨트롤러가 `MemoService::getMemo($id)` 로 직접 조회하므로, 존재하지 않으면 `ModelNotFoundException` → `messages.memo.not_found`(404). +- **응답**: `data` 는 단건 `MemoResource` 다. 필드는 `id`(integer), `uuid`(string), `title`(string), `content`(string), 타임스탬프(`created_at`/`updated_at`), 그리고 `BaseApiResource` 공통 메타 `is_owner`(boolean) + `abilities`(`can_create`/`can_update`/`can_delete` — 각 권한 보유 여부). 권한 능력은 `gnuboard7-hello_module.memos.{create,update,delete}` 권한 매핑에서 파생된다. +- **미설치 주의**: 이 문서는 미설치 상태 정적 분석으로 생성되어 실측 응답이 없다(`unresolved-path-param` — 실측할 실제 메모 레코드가 없음). 설치 후 `--seed` 실측 시 실제 값으로 채워진다. + +**응답 예시** (정적 — MemoResource 구조 기준) + +```json +{ + "success": true, + "data": { + "id": 1, + "uuid": "…", + "title": "샘플 메모", + "content": "본문", + "created_at": "…", + "updated_at": "…", + "is_owner": false, + "abilities": { "can_create": false, "can_update": false, "can_delete": false } + }, + "message": "메모를 조회했습니다.", + "error": null +} +``` + + diff --git a/modules/_bundled/sirsoft-board/docs/api/activity-stats.md b/modules/_bundled/sirsoft-board/docs/api/activity-stats.md new file mode 100644 index 00000000..348cac91 --- /dev/null +++ b/modules/_bundled/sirsoft-board/docs/api/activity-stats.md @@ -0,0 +1,50 @@ +# Activity Stats API 레퍼런스 + +> **소유**: module `sirsoft-board` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Activity Stats 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-board/me/activity-stats + +- **라우트명**: `api.modules.sirsoft-board.me.activity-stats` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\UserActivityController@stats` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| total_posts | integer | `1` | 회원 본인이 작성한 게시글 총수입니다. 비활성 게시판의 글은 제외하며 소프트 삭제된 글도 집계에서 빠집니다. | +| total_comments | integer | `0` | 회원 본인이 작성한 게시글들의 댓글 수 합계(SUM of comments_count)입니다. 비활성 게시판 글은 제외됩니다. | +| total_views | integer | `42` | 회원 본인이 작성한 게시글들의 누적 조회수 합계(SUM of view_count)입니다. 비활성 게시판 글은 제외됩니다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 로그인한 회원 본인의 게시판 활동 통계(작성 글 수, 작성 댓글 수, 누적 조회수)를 마이페이지 요약 카드에 표시하기 위해 반환합니다. `auth:sanctum` 인증이 필요한 회원 전용 엔드포인트로, 대상은 항상 인증된 본인(`Auth::id()`)입니다. + + diff --git a/modules/_bundled/sirsoft-board/docs/api/board-activities.md b/modules/_bundled/sirsoft-board/docs/api/board-activities.md new file mode 100644 index 00000000..0bbc4cc9 --- /dev/null +++ b/modules/_bundled/sirsoft-board/docs/api/board-activities.md @@ -0,0 +1,60 @@ +# Board Activities API 레퍼런스 + +> **소유**: module `sirsoft-board` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Board Activities 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-board/me/board-activities + +- **라우트명**: `api.modules.sirsoft-board.me.board-activities.index` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\UserActivityController@index` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `237` | 기본 키 (내부 식별자) | +| board_slug | string | `apidoc-sample-board` | 게시글이 속한 게시판의 슬러그(URL 식별자)입니다. 게시판 상세 링크 구성에 사용합니다. | +| board_name | string | `API 문서 샘플 게시판` | 게시글이 속한 게시판의 표시 이름입니다. 현재 로케일에 맞는 다국어 이름(`getLocalizedName()`)이 적용됩니다. | +| activity_type | string | `authored` | 활동 유형입니다. `authored`(본인이 작성한 글) 또는 `commented`(본인이 댓글을 단 글)로, 요청의 `activity_type` 필터(기본 authored)에 대응합니다. | +| activity_count | integer | `0` | activity 개수 (집계) | +| title | string | `API 문서 샘플 게시글` | 제목 | +| is_secret | boolean | `false` | secret 여부 | +| status | string | `published` | 게시글 상태입니다. `published`(공개), `blinded`(블라인드 처리), `deleted`(삭제) 등 PostStatus 값이며, UI에서 블라인드/삭제 배지 표시에 사용합니다. | +| view_count | integer | `43` | view 개수 (집계) | +| comment_count | integer | `0` | comment 개수 (집계) | +| created_at | string | `2026-07-07 09:34:50` | 생성 일시 | +| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| content_plain | string | `API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.` | 게시글 본문의 순수 텍스트입니다. HTML 모드 글은 태그를 제거한 평문으로 변환되며, 목록 미리보기용으로 사용합니다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 로그인한 회원 본인의 게시글 활동을 마이페이지에 표시하기 위해 반환합니다. `auth:sanctum` 인증이 필요한 회원 전용 엔드포인트로, 대상 사용자는 항상 인증된 본인(`Auth::id()`)입니다. `board_slug`·`search`·`activity_type`·`sort`(latest/oldest/views) 필터와 `per_page`(기본 20) 페이지네이션을 지원하며, 응답에는 적용된 필터가 `query` 로 함께 담깁니다. + + diff --git a/modules/_bundled/sirsoft-board/docs/api/board-types.md b/modules/_bundled/sirsoft-board/docs/api/board-types.md new file mode 100644 index 00000000..f3393e2d --- /dev/null +++ b/modules/_bundled/sirsoft-board/docs/api/board-types.md @@ -0,0 +1,139 @@ +# Board Types API 레퍼런스 + +> **소유**: module `sirsoft-board` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Board Types 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-board/admin/board-types + +- **라우트명**: `api.modules.sirsoft-board.admin.board-types.index` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardTypeController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.boards.create` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| slug | string | `basic` | 유형 식별자 (basic, card, gallery 등) | +| name | object | `{"ko":"기본형","en":"Basic List","ja":"基本形"}` | 유형명 (다국어: {"ko": "기본형", "en": "Basic List"}) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.create`)이 없는 경우 | + + + +**설명** 관리자가 게시판 생성/편집 화면에서 선택할 수 있는 게시판 유형(basic, card, gallery 등) 목록을 반환합니다. `auth:sanctum` 인증과 `sirsoft-board.boards.create` 권한이 필요하며, 게시판 생성 권한을 재사용해 접근을 통제합니다. `name` 은 다국어 객체로 반환되므로 표시 시 현재 로케일 키를 선택해야 합니다. + + +### POST /api/modules/sirsoft-board/admin/board-types + +- **라우트명**: `api.modules.sirsoft-board.admin.board-types.store` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardTypeController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.boards.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | body | string | 예 | max 50 | URL 친화 식별자 (slug) | +| name | body | string | 예 | — | 대상의 이름/명칭 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 새 게시판 유형을 생성합니다. `slug` 는 유형을 식별하는 고유 문자열(최대 50자)이고 `name` 은 유형명입니다. `auth:sanctum` 인증과 `sirsoft-board.boards.create` 권한이 필요하며, 성공 시 생성된 유형 리소스와 함께 201 을 반환합니다. + + +### DELETE /api/modules/sirsoft-board/admin/board-types/{id} + +- **라우트명**: `api.modules.sirsoft-board.admin.board-types.destroy` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardTypeController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.boards.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.create`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 지정한 `id` 의 게시판 유형을 삭제합니다. `auth:sanctum` 인증과 `sirsoft-board.boards.create` 권한이 필요합니다. 존재하지 않는 id 는 404, 해당 유형을 사용 중인 게시판이 있는 등 삭제할 수 없는 경우 422 를 반환합니다. + + +### PUT /api/modules/sirsoft-board/admin/board-types/{id} + +- **라우트명**: `api.modules.sirsoft-board.admin.board-types.update` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardTypeController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.boards.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| name | body | string | 예 | — | 대상의 이름/명칭 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 지정한 `id` 의 게시판 유형명을 수정합니다. `slug` 는 변경되지 않으며 `name` 만 갱신합니다. `auth:sanctum` 인증과 `sirsoft-board.boards.create` 권한이 필요하고, 존재하지 않는 id 는 404 를 반환합니다. + + diff --git a/modules/_bundled/sirsoft-board/docs/api/board.md b/modules/_bundled/sirsoft-board/docs/api/board.md new file mode 100644 index 00000000..4ed6fcb6 --- /dev/null +++ b/modules/_bundled/sirsoft-board/docs/api/board.md @@ -0,0 +1,674 @@ +# Board API 레퍼런스 + +> **소유**: module `sirsoft-board` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Board 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### POST /api/modules/sirsoft-board/admin/board/{slug}/attachments + +- **라우트명**: `api.modules.sirsoft-board.admin.board.attachments.upload` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\AttachmentController@upload` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.attachments.upload` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| file | body | file | 예 | — | 업로드 파일 | +| post_id | body | integer | 아니오 | min 1 | post 식별자 | +| collection | body | string | 아니오 | max 100 | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) | +| temp_key | body | string | 아니오 | max 64 | 게시글 작성 전 임시 업로드 세션 키. `post_id`가 없을 때 이 키로 첨부를 임시 보관했다가 게시글 저장 시점에 연결합니다 (쿼리스트링으로 보내면 body로 병합). | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.attachment.upload_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.attachments.upload`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 게시글 첨부파일 1건을 업로드합니다. `auth:sanctum` + admin + 게시판별 `attachments.upload` 권한이 필요하며, `AttachmentService::upload()`가 게시판별 동적 첨부 테이블에 저장합니다. `post_id`가 있으면 해당 게시글에 즉시 귀속되고, 없으면 `temp_key`로 임시 업로드되어 게시글 작성/수정 저장 시점에 연결됩니다. 응답은 FileUploader 컴포넌트 호환을 위해 `data.data`로 한 번 더 감싸 파일 메타(hash·url·order 등)를 반환합니다. + + +### GET /api/modules/sirsoft-board/admin/board/{slug}/attachments/download/{hash} + +- **라우트명**: `api.modules.sirsoft-board.admin.board.attachments.download` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\AttachmentController@download` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.attachments.download` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.attachments.download`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 첨부파일을 해시로 조회해 다운로드합니다. `auth:sanctum` + admin + 게시판별 `attachments.download` 권한이 필요하며, `AttachmentService::getByHash()`로 대상을 찾은 뒤 `download()`가 파일 스트림 응답을 생성합니다. 해시에 해당하는 첨부가 없거나 실제 파일이 없으면 404를 반환하고, JSON이 아닌 `StreamedResponse`로 파일 본문을 직접 전송합니다. + + +### PATCH /api/modules/sirsoft-board/admin/board/{slug}/attachments/reorder + +- **라우트명**: `api.modules.sirsoft-board.admin.board.attachments.reorder` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\AttachmentController@reorder` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.attachments.upload` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| order | body | array | 예 | min 1 | 첨부파일 순서 배열. FileUploader가 보내는 `[{id, order}]` 형태로, 각 원소의 `id`(첨부 ID)와 `order`(0 이상 정수)를 담아 표시 순서를 지정합니다. | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.attachment.reorder_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.attachments.upload`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 게시글 첨부파일들의 표시 순서를 일괄 변경합니다. `auth:sanctum` + admin + 게시판별 `attachments.upload` 권한이 필요하며, FileUploader가 보낸 `[{id, order}]` 배열을 `[ID => order]` 매핑으로 변환해 `AttachmentService::reorder()`가 게시판별 첨부 테이블의 order 값을 갱신합니다. + + +### DELETE /api/modules/sirsoft-board/admin/board/{slug}/attachments/{id} + +- **라우트명**: `api.modules.sirsoft-board.admin.board.attachments.destroy` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\AttachmentController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.attachments.upload` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.attachments.upload`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 첨부파일 1건을 삭제합니다. `auth:sanctum` + admin + 게시판별 `attachments.upload` 권한이 필요하며, `AttachmentService::getById()`로 대상 존재를 확인한 뒤 `delete()`가 게시판별 첨부 테이블 레코드와 실제 파일을 함께 제거합니다. 첨부가 없으면 404, 삭제 실패 시 500을 반환합니다. + + +### GET /api/modules/sirsoft-board/admin/board/{slug}/posts + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.index` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\PostController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.posts.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `237` | 기본 키 (내부 식별자) | +| category | null | `null` | 게시글 분류(카테고리) 문자열. 게시판이 카테고리를 쓰지 않거나 미지정 시 null (최대 50자). | +| author | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","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` | 게시글 상태 코드. `published`(게시됨) / `blinded`(블라인드) / `deleted`(삭제됨) 중 하나이며, `status_label`이 사람이 읽는 라벨입니다. | +| 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 | string | `/api/modules/sirsoft-board/boards/api…` | 썸네일 이미지 URL/경로 | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| is_reply | boolean | `false` | reply 여부 | +| created_at | string | `2026-07-07 09:34:50` | 생성 일시 | +| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| is_author | boolean | `true` | author 여부 | +| is_guest_post | boolean | `false` | guest post 여부 | +| slug | string | `apidoc-sample-board` | 게시판 슬러그 (URL/테이블명) | +| title | string | `API 문서 샘플 게시글` | 제목 | +| deleted_at | null | `null` | 소프트 삭제 일시 (미삭제 시 null) | +| content_preview | string | `API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.` | 목록용 본문 요약(태그 제거 후 앞 150자). 블라인드·비밀글은 원문 유출 방지를 위해 권한과 무관하게 빈 문자열을 반환합니다. | +| row_type | string | `normal` | 목록 행 유형. `notice`(공지) / `reply`(답변글) / `normal`(일반) 중 하나로, 목록 렌더링 시 행 스타일과 순번 표시를 분기합니다. | +| number | integer | `1` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| show_category | boolean | `false` | 목록에 카테고리 열을 노출할지 여부. 게시판 설정(`show_category`)에서 파생되어 각 행에 부여됩니다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.posts.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 특정 게시판의 게시글 목록을 조회합니다. `auth:sanctum` + admin + 게시판별 `posts.read` 권한이 필요하며, 요청 파라미터로 검색·상태·정렬·페이지네이션이 적용됩니다(`PostService::buildListParams`). 추가로 `admin.manage` 권한이 있으면 소프트 삭제된 게시글까지 포함해 조회하며, 응답에는 공지 고정 처리 후의 일반 게시글 총 건수(캐시 기반)와 관리자용 게시판 정보(`boardInfo`)가 함께 담깁니다. + + +### POST /api/modules/sirsoft-board/admin/board/{slug}/posts + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.store` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\PostController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.posts.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.post.store_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.posts.write`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 새 게시글을 작성합니다. `auth:sanctum` + admin + 게시판별 `posts.write` 권한이 필요하며, `StorePostRequest` 검증을 거친 값에 작성자(`Auth::id()`)와 요청 IP가 자동으로 채워집니다. 업로드 파일과 첨부파일 ID 배열은 본문에서 분리되어 `PostService::createPost()`로 전달되고, 성공 시 생성된 게시글 리소스를 201로 반환합니다. + + +### GET /api/modules/sirsoft-board/admin/board/{slug}/posts/form-data + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.form-data` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\PostController@getFormData` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.posts.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| title | string | `` | 제목 | +| content | string | `` | 본문 내용 | +| content_mode | string | `text` | 본문 편집 모드. `html`(위지윅/HTML) 또는 `text`(평문)이며, 폼 초기값은 `text`입니다. | +| category | null | `null` | 게시글 분류(카테고리) 문자열. 게시판이 카테고리를 쓰지 않거나 미지정 시 null (최대 50자). | +| is_notice | boolean | `false` | notice 여부 | +| is_secret | boolean | `false` | secret 여부 | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.posts.write`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글 작성/수정/답변글 폼에 미리 채울 입력 데이터를 반환합니다. `auth:sanctum` + admin + 게시판별 `posts.write` 권한이 필요하며, 쿼리 파라미터에 따라 분기합니다. `post_id`가 있으면 기존 게시글 데이터(수정 모드), `parent_id`가 있으면 제목에 `Re:`를 붙이고 원글 카테고리·비밀글 여부를 물려받은 답변글 기본값(답글 허용 게시판만, 아니면 404), 둘 다 없으면 빈 폼(게시판 `secret_mode`가 `always`면 비밀글 기본값)을 돌려줍니다. + + +### GET /api/modules/sirsoft-board/admin/board/{slug}/posts/form-meta + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.form-meta` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\PostController@getFormMeta` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.posts.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| board | object | `{"id":12,"name":"API 문서 샘플 게시판","slug":"apidoc-sample-boa…` | 폼 화면 표시에 필요한 게시판 정보 객체(이름·슬러그·댓글/답글/비밀글 설정 등). 사용자 권한(abilities)과 함께 항상 포함됩니다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.posts.write`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글 폼 화면 표시용 메타 데이터(읽기 전용)를 반환합니다. `auth:sanctum` + admin + 게시판별 `posts.write` 권한이 필요하며, 게시판 정보와 사용자 권한(abilities)을 항상 포함합니다. `post_id`가 있으면 작성자·작성일·첨부파일과 원글 정보를 덧붙이고(수정 모드), `parent_id`가 있으면 원글 정보를 포함하되 블라인드/삭제된 원글에는 답글 작성이 차단됩니다(각각 403). + + +### DELETE /api/modules/sirsoft-board/admin/board/{slug}/posts/{id} + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.destroy` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\PostController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.posts.write|sirsoft-board.{slug}.admin.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.posts.write|sirsoft-board.{slug}.admin.manage`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 게시글 1건을 소프트 삭제합니다. `auth:sanctum` + admin 인증이 필요하며, 라우트 권한은 `posts.write` 또는 `manage`입니다. 컨트롤러가 대상 게시글을 조회한 뒤 세분화된 권한 분기를 적용합니다: `admin.manage`는 모든 글(비회원 글 포함)을, `admin.posts.write`는 본인 글만 삭제할 수 있으며 이미 삭제된 글의 재처리는 `admin.manage`가 필요합니다. `PostService::deletePost()`가 'admin' 컨텍스트로 소프트 삭제를 수행합니다. + + +### GET /api/modules/sirsoft-board/admin/board/{slug}/posts/{id} + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.show` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\PostController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.posts.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `237` | 기본 키 (내부 식별자) | +| category | null | `null` | 게시글 분류(카테고리) 문자열. 게시판이 카테고리를 쓰지 않거나 미지정 시 null (최대 50자). | +| author | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","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` | 게시글 상태 코드. `published`(게시됨) / `blinded`(블라인드) / `deleted`(삭제됨) 중 하나이며, `status_label`이 사람이 읽는 라벨입니다. | +| 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 | string | `/api/modules/sirsoft-board/boards/api…` | 썸네일 이미지 URL/경로 | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| is_reply | boolean | `false` | reply 여부 | +| created_at | string | `2026-07-07 09:34:50` | 생성 일시 | +| 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 | `a231747f-e82e-4cf2-9ae1-a261849dce40` | user 식별자 (연관 리소스 참조) | +| trigger_type | string | `user` | 상태 변경(삭제/블라인드 등)을 유발한 주체. `report`(신고) / `admin`(관리자 직권) / `system`(시스템) / `auto_hide`(신고 누적 자동 블라인드) / `user`(사용자 직접) / `cascade`(상위 삭제 연쇄) 중 하나입니다. | +| updated_at | string | `2026-07-07 09:39:03` | 최종 수정 일시 | +| 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 | object | `{"slug":"apidoc-sample-board","name":"API 문서 샘플 게시판","typ…` | 소속 게시판 정보 객체(슬러그·이름·유형·댓글/답글/신고 사용 여부·조회수 표시·최대 답글/댓글 깊이·신고 사유 목록). board 관계가 로드된 경우에만 채워지며, 아니면 null. | +| navigation | object | `{"prev":null,"next":null}` | 이전/다음 게시글 이동 정보. `prev`·`next` 키에 인접 게시글 요약(없으면 null)이 담기며, 상세 로드 시 함께 계산됩니다. | +| parent | null | `null` | 상위 항목 객체 (parent 관계 파생) | +| comments | array | `[{"id":760,"post_id":237,"parent_id":null,"content":"API …` | 게시글에 달린 댓글 목록(CommentResource 컬렉션). comments 관계가 로드된 경우에만 채워지며, 각 항목에 신고 여부가 사전 로드되어 담깁니다. | +| attachments | array | `[{"id":155,"hash":"apidocsmpl1","original_filename":"apid…` | 게시글 첨부파일 목록(AttachmentResource 컬렉션). 비밀글은 열람 권한이 없으면 빈 배열, 삭제된 게시글은 관리 권한이 없으면 연쇄 삭제된 첨부만 노출됩니다. | +| replies | array | `[]` | 이 게시글에 달린 답변글 목록(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 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.posts.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 게시글 상세를 조회합니다. `auth:sanctum` + admin + 게시판별 `posts.read` 권한이 필요하며, 삭제된 게시글은 `admin.manage` 권한이 있어야 열람할 수 있습니다(없으면 403). `PostService::loadPostDetail()`이 조회수 증가·댓글·이전/다음 게시글까지 로드하며, 응답에는 댓글별 신고 여부를 N+1 없이 일괄 사전 로드해 담습니다. + + +### PUT /api/modules/sirsoft-board/admin/board/{slug}/posts/{id} + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.update` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\PostController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.posts.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.post.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.posts.write`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 게시글을 수정합니다. `auth:sanctum` + admin + 게시판별 `posts.write` 권한이 필요하며, 컨트롤러가 대상 게시글을 조회한 뒤 세분화된 권한을 적용합니다: 일반 글은 `admin.manage`(타인 글) 또는 `admin.write`(본인 글), 이미 삭제된 글은 `admin.manage`가 필요합니다. `UpdatePostRequest` 검증 값에서 첨부파일 ID 배열을 분리해 `PostService::updatePost()`로 전달하고, 갱신된 게시글 리소스를 반환합니다. + + +### PATCH /api/modules/sirsoft-board/admin/board/{slug}/posts/{id}/blind + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.blind` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\PostController@blind` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| reason | body | string | 아니오 | max 1000 | 블라인드 처리 사유(최대 1000자). 처리 이력(action_logs)에 기록되며, 미지정 시 빈 문자열로 저장됩니다. | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.manage`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 게시글을 블라인드 처리합니다. `auth:sanctum` + admin + 게시판별 `admin.manage` 권한이 필요하며, 선택적 `reason`(최대 1000자)을 사유로 받아 `PostService::blindPost()`가 게시글 상태를 블라인드로 전환합니다. 소프트 삭제와 달리 게시글을 숨기되 관리 목적으로 보존하는 처리이며, 복원(restore)으로 되돌릴 수 있습니다. + + +### PATCH /api/modules/sirsoft-board/admin/board/{slug}/posts/{id}/restore + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.restore` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\PostController@restore` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| reason | body | string | 아니오 | max 1000 | 블라인드 복원 사유(최대 1000자). 처리 이력(action_logs)에 기록되며, 미지정 시 null로 전달됩니다. | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.manage`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 블라인드 처리된 게시글을 복원합니다. `auth:sanctum` + admin + 게시판별 `admin.manage` 권한이 필요하며, 선택적 `reason`(최대 1000자)을 사유로 받아 `PostService::restorePost()`가 블라인드 상태를 해제해 게시글을 다시 노출합니다. + + +### POST /api/modules/sirsoft-board/admin/board/{slug}/posts/{postId}/comments + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.comments.store` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\CommentController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.comments.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| postId | path | string | 예 | — | 대상 post의 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.comment.store_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.comments.write`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 특정 게시글에 댓글을 작성합니다. `auth:sanctum` + admin + 게시판별 `comments.write` 권한이 필요하며, 게시판의 `use_comment`가 꺼져 있으면 403으로 차단됩니다. 검증된 값에 게시글 ID·작성자(`Auth::id()`)·요청 IP가 자동으로 채워져 `CommentService::createComment()`로 전달되고, 성공 시 생성된 댓글 리소스를 201로 반환합니다. + + +### DELETE /api/modules/sirsoft-board/admin/board/{slug}/posts/{postId}/comments/{id} + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.comments.destroy` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\CommentController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.comments.write|sirsoft-board.{slug}.admin.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| postId | path | string | 예 | — | 대상 post의 식별자 | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.comments.write|sirsoft-board.{slug}.admin.manage`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 댓글 1건을 삭제합니다. `auth:sanctum` + admin 인증이 필요하며, 라우트 권한은 `comments.write` 또는 `manage`입니다. 컨트롤러가 댓글을 조회한 뒤 권한을 적용합니다: `admin.manage`는 모든 댓글(비회원 댓글 포함), `admin.write`는 본인 댓글만 삭제할 수 있습니다. 게시판의 `use_comment`가 꺼져 있으면 403이며, `CommentService::deleteComment()`가 'admin' 컨텍스트로 삭제를 수행합니다. + + +### PUT /api/modules/sirsoft-board/admin/board/{slug}/posts/{postId}/comments/{id} + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.comments.update` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\CommentController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.comments.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| postId | path | string | 예 | — | 대상 post의 식별자 | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.comment.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.comments.write`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 댓글 1건을 수정합니다. `auth:sanctum` + admin + 게시판별 `comments.write` 권한이 필요하며, 컨트롤러가 댓글을 조회한 뒤 권한을 적용합니다: `admin.manage`는 모든 댓글, `admin.write`는 본인 댓글만 수정할 수 있습니다. 게시판의 `use_comment`가 꺼져 있으면 403이며, `UpdateCommentRequest` 검증 값으로 `CommentService::updateComment()`가 갱신을 수행합니다. + + +### PATCH /api/modules/sirsoft-board/admin/board/{slug}/posts/{postId}/comments/{id}/blind + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.comments.blind` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\CommentController@blind` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| postId | path | string | 예 | — | 대상 post의 식별자 | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| reason | body | string | 아니오 | max 1000 | 댓글 블라인드 처리 사유(최대 1000자). 처리 이력에 기록되며, 미지정 시 빈 문자열로 저장됩니다. | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.manage`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 댓글을 블라인드 처리합니다. `auth:sanctum` + admin + 게시판별 `admin.manage` 권한이 필요하며, 게시판의 `use_comment`가 꺼져 있으면 403으로 차단됩니다. 선택적 `reason`(최대 1000자)을 사유로 받아 `CommentService::blindComment()`가 댓글을 숨김 처리하되 관리 목적으로 보존하며, 복원(restore)으로 되돌릴 수 있습니다. + + +### PATCH /api/modules/sirsoft-board/admin/board/{slug}/posts/{postId}/comments/{id}/restore + +- **라우트명**: `api.modules.sirsoft-board.admin.board.posts.comments.restore` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\CommentController@restore` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.{slug}.admin.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| postId | path | string | 예 | — | 대상 post의 식별자 | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.manage`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 관리자가 블라인드 처리된 댓글을 복원합니다. `auth:sanctum` + admin + 게시판별 `admin.manage` 권한이 필요하며, 게시판의 `use_comment`가 꺼져 있으면 403으로 차단됩니다. 요청 본문의 선택적 `reason`을 사유로 받아 `CommentService::restoreComment()`가 블라인드 상태를 해제해 댓글을 다시 노출합니다. + + diff --git a/modules/_bundled/sirsoft-board/docs/api/boards.md b/modules/_bundled/sirsoft-board/docs/api/boards.md new file mode 100644 index 00000000..2685d377 --- /dev/null +++ b/modules/_bundled/sirsoft-board/docs/api/boards.md @@ -0,0 +1,1543 @@ +# Boards API 레퍼런스 + +> **소유**: module `sirsoft-board` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Boards 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-board/admin/boards + +- **라우트명**: `api.modules.sirsoft-board.admin.boards.index` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.boards.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `12` | 기본 키 (내부 식별자) | +| name | string | `API 문서 샘플 게시판` | 게시판명 (다국어 JSON) | +| slug | string | `apidoc-sample-board` | 게시판 슬러그 (URL/테이블명) | +| is_active | boolean | `true` | active 여부 | +| type | string | `basic` | 게시판 타입 (basic, gallery, card 등) | +| description | string | `` | 게시판 설명 (다국어 JSON) | +| per_page | integer | `20` | 페이지당 게시글 수 (PC) | +| per_page_mobile | integer | `15` | 페이지당 게시글 수 (Mobile) | +| order_by | string | `created_at` | 정렬 기준 (created_at, view_count, title, author) | +| order_direction | string | `DESC` | 정렬 방향 (ASC, DESC) | +| categories | array | `[]` | 분류 목록 (배열) | +| show_view_count | boolean | `true` | 조회수 노출 | +| secret_mode | string | `disabled` | 비밀글 설정 (disabled: 사용안함, enabled: 사용함, always: 고정) | +| use_comment | boolean | `true` | 댓글 기능 사용 | +| use_reply | boolean | `true` | 게시글 답변 기능 사용 (댓글에 대한 답글 아님) | +| max_reply_depth | integer | `5` | 답변글 최대 깊이 (1~5) | +| use_report | boolean | `true` | 게시글/댓글 신고 기능 사용 | +| comment_order | string | `ASC` | 댓글 정렬 순서 (ASC: 오름차순, DESC: 내림차순) | +| max_comment_depth | integer | `10` | 대댓글 최대 깊이 (1~10) | +| min_title_length | integer | `2` | 최소 제목 글자 수 | +| max_title_length | integer | `200` | 최대 제목 글자 수 | +| min_content_length | integer | `10` | 최소 게시글 글자 수 | +| max_content_length | integer | `10000` | 최대 게시글 글자 수 | +| min_comment_length | integer | `2` | 최소 댓글 글자 수 | +| max_comment_length | integer | `1000` | 최대 댓글 글자 수 | +| blocked_keywords | array | `[]` | 금지어 목록 (배열) | +| use_file_upload | boolean | `true` | 파일 업로드 사용 | +| max_file_size | integer | `10` | 최대 파일 크기 (MB) | +| max_file_count | integer | `5` | max file 개수 (집계) | +| allowed_extensions | array | `["jpg","jpeg","png","gif","pdf","zip"]` | 허용 확장자 배열 | +| add_to_menu | null | `null` | 관리자 메뉴 등록 여부 (폼 요청에서만 채워지는 토글 초기값 — 조회 응답에서는 항상 null) | +| new_display_hours | integer | `24` | 신규 게시글 표시 기간 (시간 단위) | +| board_managers | array | `[]` | 게시판 관리자로 지정된 사용자 목록 (manager 역할 사용자의 uuid/name/email — 역할 기반 파생) | +| board_steps | array | `[]` | 게시판 승인/처리 담당자(스텝)로 지정된 사용자 목록 (step 역할 사용자의 uuid/name/email — 역할 기반 파생) | +| board_manager_ids | array | `[]` | board manager 식별자 배열 (연관 리소스 참조) | +| board_step_ids | array | `[]` | board step 식별자 배열 (연관 리소스 참조) | +| notify_author | boolean | `true` | 작성자 이메일 알림 (댓글, 대댓글, 답변글, 관리자 처리 시) | +| notify_admin_on_post | boolean | `true` | 관리자 이메일 알림 (게시글 등록 시) | +| created_at | string | `2026-07-07 09:34:50` | 생성 일시 | +| updated_at | string | `2026-07-07 09:34:50` | 최종 수정 일시 | +| permissions | null | `null` | 연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) | +| category_post_counts | null | `null` | 분류별 게시글 개수 맵 (요청 시 조건부로만 채워지며, 미포함 시 null) | +| posts_count | integer | `0` | posts 개수 (집계) | +| 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 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.read`)이 없는 경우 | + + + +**설명** 관리자 화면의 게시판 관리 목록을 조회합니다. `auth:sanctum` + `sirsoft-board.boards.read` 권한이 필요하며, 요청의 필터/페이징 파라미터를 그대로 서비스에 넘겨 전체 게시판을 페이지네이션합니다. 각 항목은 `BoardCollection::withPermissions()` 로 감싸져 현재 관리자의 생성/수정/삭제 가능 여부(`abilities`)를 함께 반환합니다. + + +### POST /api/modules/sirsoft-board/admin/boards + +- **라우트명**: `api.modules.sirsoft-board.admin.boards.store` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.boards.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| slug | body | string | 예 | max 50 | URL 친화 식별자 (slug) | +| description | body | array | 아니오 | — | 설명 | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| type | body | string | 예 | max 50 | 게시판 타입 슬러그 (basic, gallery, card 등 — 등록된 board_types 값 중 하나) | +| add_to_menu | body | boolean | 아니오 | — | 저장 시 이 게시판을 관리자 메뉴에 자동 등록할지 여부 (DB 컬럼 아님 — 메뉴 등록 처리에만 사용) | +| per_page | body | integer | 예 | min 5, max 100 | 페이지당 항목 수 | +| per_page_mobile | body | integer | 예 | min 5, max 100 | 모바일 화면에서 페이지당 표시할 게시글 수 | +| order_by | body | string | 예 | `created_at`, `view_count`, `title`, `author` | 정렬 기준 필드명 | +| order_direction | body | string | 예 | `ASC`, `DESC` | 게시글 목록 정렬 방향 (ASC 오름차순 / DESC 내림차순) | +| categories | body | array | 아니오 | max 50 | 게시글 분류(카테고리) 이름 목록 (최대 50개, 빈/공백 이름 불가) | +| show_view_count | body | boolean | 예 | — | 게시글 목록/상세에 조회수를 노출할지 여부 | +| secret_mode | body | string | 예 | `disabled`, `enabled`, `always` | 비밀글 설정 (disabled 사용 안 함 / enabled 작성자 선택 가능 / always 전 글 비밀글 강제) | +| use_comment | body | boolean | 예 | — | 댓글 기능 사용 여부 | +| use_reply | body | boolean | 예 | — | 게시글 답변(원글에 대한 답글) 기능 사용 여부 | +| use_report | body | boolean | 예 | — | 게시글/댓글 신고 기능 사용 여부 | +| comment_order | body | string | 예 | `ASC`, `DESC` | 댓글 정렬 순서 (ASC 오름차순 / DESC 내림차순) | +| new_display_hours | body | integer | 아니오 | min 1, max 720 | 신규(NEW) 표시를 유지할 기간 (시간 단위, 최대 720시간=30일) | +| min_title_length | body | integer | 아니오 | min 0, max 200 | 게시글 제목 최소 글자 수 | +| max_title_length | body | integer | 아니오 | min 1, max 1000 | 게시글 제목 최대 글자 수 | +| min_content_length | body | integer | 아니오 | min 0, max 10000 | 게시글 본문 최소 글자 수 | +| max_content_length | body | integer | 아니오 | min 1, max 100000 | 게시글 본문 최대 글자 수 | +| min_comment_length | body | integer | 아니오 | min 0, max 1000 | 댓글 최소 글자 수 | +| max_comment_length | body | integer | 아니오 | min 1, max 10000 | 댓글 최대 글자 수 | +| use_file_upload | body | boolean | 예 | — | 파일 첨부 기능 사용 여부 (true일 때 allowed_extensions 최소 1개 필수) | +| max_file_size | body | integer | 아니오 | min 1, max 200 | 첨부파일 1개당 최대 크기 (MB 단위) | +| max_file_count | body | integer | 아니오 | min 1, max 20 | 게시글 1건당 첨부할 수 있는 최대 파일 개수 | +| allowed_extensions | body | array | 예 | min 1 | 업로드 허용 확장자 목록 (예: jpg, png, pdf — 첨부 사용 시 최소 1개 필수) | +| board_manager_ids | body | array | 예 | min 1 | board manager 식별자 배열 | +| board_step_ids | body | array | 아니오 | — | board step 식별자 배열 | +| permissions | body | array | 아니오 | — | 게시판별 세부 권한 매트릭스 (권한 키별 mode/roles — 미지정 시 Service가 Manager/Step 역할을 주입) | +| max_reply_depth | body | integer | 아니오 | min 1, max 10 | 답변글 최대 중첩 깊이 | +| max_comment_depth | body | integer | 아니오 | min 0, max 10 | 대댓글 최대 중첩 깊이 | +| notify_admin_on_post | body | boolean | 예 | — | 게시글 등록 시 관리자에게 이메일 알림 발송 여부 | +| notify_author | body | boolean | 예 | — | 댓글·대댓글·답변글·관리자 처리 발생 시 작성자에게 이메일 알림 발송 여부 | +| blocked_keywords | body | array | 아니오 | — | 게시글/댓글 작성 시 차단할 금지어 목록 (각 항목 최대 100자, 쉼표 구분 문자열도 허용) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.board.store_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 새 게시판을 생성합니다. `auth:sanctum` + `sirsoft-board.boards.create` 권한이 필요하며, `StoreBoardRequest` 로 검증된 이름/슬러그/타입/권한/제한값 등을 받아 게시판과 전용 데이터를 초기화합니다. 슬러그는 게시글 테이블명·권한 키·URL의 기준이 되므로 생성 후 변경할 수 없다는 점에 주의하고, 성공 시 생성된 게시판 리소스를 201로 반환합니다. + + +### GET /api/modules/sirsoft-board/admin/boards/form-data + +- **라우트명**: `api.modules.sirsoft-board.admin.boards.form-data` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardController@getFormData` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.boards.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| type | string | `basic` | 게시판 타입 (basic, gallery, card 등) | +| per_page | integer | `20` | 페이지당 게시글 수 (PC) | +| per_page_mobile | integer | `15` | 페이지당 게시글 수 (Mobile) | +| order_by | string | `created_at` | 정렬 기준 (created_at, view_count, title, author) | +| order_direction | string | `DESC` | 정렬 방향 (ASC, DESC) | +| secret_mode | string | `disabled` | 비밀글 설정 (disabled: 사용안함, enabled: 사용함, always: 고정) | +| use_comment | boolean | `true` | 댓글 기능 사용 | +| use_reply | boolean | `true` | 게시글 답변 기능 사용 (댓글에 대한 답글 아님) | +| max_reply_depth | integer | `5` | 답변글 최대 깊이 (1~5) | +| max_comment_depth | integer | `10` | 대댓글 최대 깊이 (1~10) | +| comment_order | string | `ASC` | 댓글 정렬 순서 (ASC: 오름차순, DESC: 내림차순) | +| show_view_count | boolean | `true` | 조회수 노출 | +| use_report | boolean | `false` | 게시글/댓글 신고 기능 사용 | +| min_title_length | integer | `2` | 최소 제목 글자 수 | +| max_title_length | integer | `200` | 최대 제목 글자 수 | +| min_content_length | integer | `2` | 최소 게시글 글자 수 | +| max_content_length | integer | `10000` | 최대 게시글 글자 수 | +| min_comment_length | integer | `2` | 최소 댓글 글자 수 | +| max_comment_length | integer | `1000` | 최대 댓글 글자 수 | +| use_file_upload | boolean | `false` | 파일 업로드 사용 | +| max_file_size | integer | `10` | 최대 파일 크기 (MB) | +| max_file_count | integer | `5` | max file 개수 (집계) | +| notify_admin_on_post | boolean | `true` | 관리자 이메일 알림 (게시글 등록 시) | +| notify_author | boolean | `true` | 작성자 이메일 알림 (댓글, 대댓글, 답변글, 관리자 처리 시) | +| new_display_hours | integer | `24` | 신규 게시글 표시 기간 (시간 단위) | +| id | null | `null` | 기본 키 (내부 식별자) | +| slug | null | `null` | 게시판 슬러그 (URL/테이블명) | +| is_active | boolean | `true` | active 여부 | +| add_to_menu | boolean | `false` | 관리자 메뉴 등록 여부 폼 토글 초기값 (수정 모드에서 해당 게시판이 이미 관리자 메뉴에 등록돼 있으면 true) | +| board_managers | array | `[{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"A…` | 게시판 관리자로 지정된 사용자 목록 (생성 모드에서는 현재 로그인 관리자가 기본값으로 채워짐 — uuid/name/email) | +| board_steps | array | `[]` | 게시판 승인/처리 담당자(스텝)로 지정된 사용자 목록 (step 역할 사용자의 uuid/name/email — 역할 기반 파생) | +| board_manager_ids | array | `["a231747f-e82e-4cf2-9ae1-a261849dce40"]` | board manager 식별자 배열 (연관 리소스 참조) | +| board_step_ids | array | `[]` | board step 식별자 배열 (연관 리소스 참조) | +| created_at | null | `null` | 생성 일시 | +| updated_at | null | `null` | 최종 수정 일시 | +| name | object | `{"ko":""}` | 게시판명 (다국어 JSON) | +| description | object | `{"ko":""}` | 게시판 설명 (다국어 JSON) | +| categories | array | `[]` | 분류 목록 (배열) | +| blocked_keywords | array | `[]` | 금지어 목록 (배열) | +| allowed_extensions | array | `["jpg","jpeg","png","gif","webp","pdf","doc","docx","xls"…` | 허용 확장자 배열 | +| permissions | object | `{"admin_posts_read":{"_key":"admin_posts_read","name":"ad…` | 연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) | +| 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 범위) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.read`)이 없는 경우 | + + + +**설명** 게시판 생성/수정 폼을 렌더링하는 데 필요한 초기 데이터를 반환합니다. `auth:sanctum` + `sirsoft-board.boards.read` 권한이 필요하며, 쿼리 파라미터로 모드가 분기됩니다: `board_id` 가 있으면 기존 게시판 값(수정 모드), `copy_id` 가 있으면 복사 원본 값(복사 모드), 둘 다 없으면 모듈 기본 설정 기반 기본값(생성 모드)을 채웁니다. 생성 모드에서는 로그인한 관리자가 게시판 관리자 기본값으로 지정되며, 응답에는 선택 가능한 게시판 타입 목록(`board_types`)과 입력 한계값(`_meta.limits`)이 함께 포함됩니다. + + +### GET /api/modules/sirsoft-board/admin/boards/slug/{slug} + +- **라우트명**: `api.modules.sirsoft-board.admin.boards.show-by-slug` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardController@showBySlug` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.boards.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `12` | 기본 키 (내부 식별자) | +| name | string | `API 문서 샘플 게시판` | 게시판명 (다국어 JSON) | +| slug | string | `apidoc-sample-board` | 게시판 슬러그 (URL/테이블명) | +| is_active | boolean | `true` | active 여부 | +| type | string | `basic` | 게시판 타입 (basic, gallery, card 등) | +| description | string | `` | 게시판 설명 (다국어 JSON) | +| per_page | integer | `20` | 페이지당 게시글 수 (PC) | +| per_page_mobile | integer | `15` | 페이지당 게시글 수 (Mobile) | +| order_by | string | `created_at` | 정렬 기준 (created_at, view_count, title, author) | +| order_direction | string | `DESC` | 정렬 방향 (ASC, DESC) | +| categories | array | `[]` | 분류 목록 (배열) | +| show_view_count | boolean | `true` | 조회수 노출 | +| secret_mode | string | `disabled` | 비밀글 설정 (disabled: 사용안함, enabled: 사용함, always: 고정) | +| use_comment | boolean | `true` | 댓글 기능 사용 | +| use_reply | boolean | `true` | 게시글 답변 기능 사용 (댓글에 대한 답글 아님) | +| max_reply_depth | integer | `5` | 답변글 최대 깊이 (1~5) | +| use_report | boolean | `true` | 게시글/댓글 신고 기능 사용 | +| comment_order | string | `ASC` | 댓글 정렬 순서 (ASC: 오름차순, DESC: 내림차순) | +| max_comment_depth | integer | `10` | 대댓글 최대 깊이 (1~10) | +| min_title_length | integer | `2` | 최소 제목 글자 수 | +| max_title_length | integer | `200` | 최대 제목 글자 수 | +| min_content_length | integer | `10` | 최소 게시글 글자 수 | +| max_content_length | integer | `10000` | 최대 게시글 글자 수 | +| min_comment_length | integer | `2` | 최소 댓글 글자 수 | +| max_comment_length | integer | `1000` | 최대 댓글 글자 수 | +| blocked_keywords | array | `[]` | 금지어 목록 (배열) | +| use_file_upload | boolean | `true` | 파일 업로드 사용 | +| max_file_size | integer | `10` | 최대 파일 크기 (MB) | +| max_file_count | integer | `5` | max file 개수 (집계) | +| allowed_extensions | array | `["jpg","jpeg","png","gif","pdf","zip"]` | 허용 확장자 배열 | +| add_to_menu | null | `null` | 관리자 메뉴 등록 여부 (폼 요청에서만 채워지는 토글 초기값 — 조회 응답에서는 항상 null) | +| new_display_hours | integer | `24` | 신규 게시글 표시 기간 (시간 단위) | +| board_managers | array | `[]` | 게시판 관리자로 지정된 사용자 목록 (manager 역할 사용자의 uuid/name/email — 역할 기반 파생) | +| board_steps | array | `[]` | 게시판 승인/처리 담당자(스텝)로 지정된 사용자 목록 (step 역할 사용자의 uuid/name/email — 역할 기반 파생) | +| board_manager_ids | array | `[]` | board manager 식별자 배열 (연관 리소스 참조) | +| board_step_ids | array | `[]` | board step 식별자 배열 (연관 리소스 참조) | +| notify_author | boolean | `true` | 작성자 이메일 알림 (댓글, 대댓글, 답변글, 관리자 처리 시) | +| notify_admin_on_post | boolean | `true` | 관리자 이메일 알림 (게시글 등록 시) | +| created_at | string | `2026-07-07 09:34:50` | 생성 일시 | +| updated_at | string | `2026-07-07 09:34:50` | 최종 수정 일시 | +| permissions | null | `null` | 연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) | +| category_post_counts | null | `null` | 분류별 게시글 개수 맵 (요청 시 조건부로만 채워지며, 미포함 시 null) | +| posts_count | integer | `0` | posts 개수 (집계) | +| 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 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 슬러그로 게시판 상세를 조회합니다(관리자용). `auth:sanctum` + `sirsoft-board.boards.read` 권한이 필요하며, ID 대신 슬러그로 접근하는 화면(게시글 관리·글쓰기 진입 등)에서 사용합니다. 관리자 라우트이므로 항상 사용자 권한 맵(`user_abilities`)을 포함하며, `parent_id` 쿼리 파라미터가 있으면 답변글 작성을 위해 원글 정보와 기본 제목(`RE: ...`)을 `parent_post` 로 함께 반환합니다. + + +### DELETE /api/modules/sirsoft-board/admin/boards/{board} + +- **라우트명**: `api.modules.sirsoft-board.admin.boards.destroy` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.boards.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| board | path | string | 예 | — | 대상 board의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판을 삭제합니다. `auth:sanctum` + `sirsoft-board.boards.delete` 권한이 필요하며, 요청의 `force_delete` 불리언 플래그로 삭제 방식을 결정합니다. 게시판 삭제는 소속 게시글·댓글·첨부·권한까지 연쇄 정리를 수반하므로 되돌릴 수 없다는 점에 주의해야 합니다. + + +### GET /api/modules/sirsoft-board/admin/boards/{board} + +- **라우트명**: `api.modules.sirsoft-board.admin.boards.show` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.boards.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| board | path | string | 예 | — | 대상 board의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `12` | 기본 키 (내부 식별자) | +| name | string | `API 문서 샘플 게시판` | 게시판명 (다국어 JSON) | +| slug | string | `apidoc-sample-board` | 게시판 슬러그 (URL/테이블명) | +| is_active | boolean | `true` | active 여부 | +| type | string | `basic` | 게시판 타입 (basic, gallery, card 등) | +| description | string | `` | 게시판 설명 (다국어 JSON) | +| per_page | integer | `20` | 페이지당 게시글 수 (PC) | +| per_page_mobile | integer | `15` | 페이지당 게시글 수 (Mobile) | +| order_by | string | `created_at` | 정렬 기준 (created_at, view_count, title, author) | +| order_direction | string | `DESC` | 정렬 방향 (ASC, DESC) | +| categories | array | `[]` | 분류 목록 (배열) | +| show_view_count | boolean | `true` | 조회수 노출 | +| secret_mode | string | `disabled` | 비밀글 설정 (disabled: 사용안함, enabled: 사용함, always: 고정) | +| use_comment | boolean | `true` | 댓글 기능 사용 | +| use_reply | boolean | `true` | 게시글 답변 기능 사용 (댓글에 대한 답글 아님) | +| max_reply_depth | integer | `5` | 답변글 최대 깊이 (1~5) | +| use_report | boolean | `true` | 게시글/댓글 신고 기능 사용 | +| comment_order | string | `ASC` | 댓글 정렬 순서 (ASC: 오름차순, DESC: 내림차순) | +| max_comment_depth | integer | `10` | 대댓글 최대 깊이 (1~10) | +| min_title_length | integer | `2` | 최소 제목 글자 수 | +| max_title_length | integer | `200` | 최대 제목 글자 수 | +| min_content_length | integer | `10` | 최소 게시글 글자 수 | +| max_content_length | integer | `10000` | 최대 게시글 글자 수 | +| min_comment_length | integer | `2` | 최소 댓글 글자 수 | +| max_comment_length | integer | `1000` | 최대 댓글 글자 수 | +| blocked_keywords | array | `[]` | 금지어 목록 (배열) | +| use_file_upload | boolean | `true` | 파일 업로드 사용 | +| max_file_size | integer | `10` | 최대 파일 크기 (MB) | +| max_file_count | integer | `5` | max file 개수 (집계) | +| allowed_extensions | array | `["jpg","jpeg","png","gif","pdf","zip"]` | 허용 확장자 배열 | +| add_to_menu | null | `null` | 관리자 메뉴 등록 여부 (폼 요청에서만 채워지는 토글 초기값 — 조회 응답에서는 항상 null) | +| new_display_hours | integer | `24` | 신규 게시글 표시 기간 (시간 단위) | +| board_managers | array | `[]` | 게시판 관리자로 지정된 사용자 목록 (manager 역할 사용자의 uuid/name/email — 역할 기반 파생) | +| board_steps | array | `[]` | 게시판 승인/처리 담당자(스텝)로 지정된 사용자 목록 (step 역할 사용자의 uuid/name/email — 역할 기반 파생) | +| board_manager_ids | array | `[]` | board manager 식별자 배열 (연관 리소스 참조) | +| board_step_ids | array | `[]` | board step 식별자 배열 (연관 리소스 참조) | +| notify_author | boolean | `true` | 작성자 이메일 알림 (댓글, 대댓글, 답변글, 관리자 처리 시) | +| notify_admin_on_post | boolean | `true` | 관리자 이메일 알림 (게시글 등록 시) | +| created_at | string | `2026-07-07 09:34:50` | 생성 일시 | +| updated_at | string | `2026-07-07 09:34:50` | 최종 수정 일시 | +| permissions | object | `{"admin_posts_read":{"_key":"admin_posts_read","name":"ad…` | 연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) | +| category_post_counts | null | `null` | 분류별 게시글 개수 맵 (요청 시 조건부로만 채워지며, 미포함 시 null) | +| posts_count | integer | `0` | posts 개수 (집계) | +| 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 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** ID로 게시판 상세를 조회합니다(관리자용). `auth:sanctum` + `sirsoft-board.boards.read` 권한이 필요하며, 게시판 설정 편집·상세 확인 화면에서 사용합니다. 게시판 권한 매트릭스(`permissions`)를 포함한 전체 설정 필드를 반환합니다. + + +### PUT /api/modules/sirsoft-board/admin/boards/{board} + +- **라우트명**: `api.modules.sirsoft-board.admin.boards.update` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.boards.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| board | path | string | 예 | — | 대상 board의 식별자 | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| description | body | array | 아니오 | — | 설명 | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| type | body | string | 예 | max 50 | 게시판 타입 슬러그 (basic, gallery, card 등 — 등록된 board_types 값 중 하나) | +| add_to_menu | body | boolean | 아니오 | — | 저장 시 이 게시판을 관리자 메뉴에 자동 등록할지 여부 (DB 컬럼 아님 — 메뉴 등록 처리에만 사용) | +| per_page | body | integer | 예 | min 5, max 100 | 페이지당 항목 수 | +| per_page_mobile | body | integer | 예 | min 5, max 100 | 모바일 화면에서 페이지당 표시할 게시글 수 | +| order_by | body | string | 예 | `created_at`, `view_count`, `title`, `author` | 정렬 기준 필드명 | +| order_direction | body | string | 예 | `ASC`, `DESC` | 게시글 목록 정렬 방향 (ASC 오름차순 / DESC 내림차순) | +| categories | body | array | 아니오 | max 50 | 게시글 분류(카테고리) 이름 목록 (최대 50개, 빈/공백 이름 불가) | +| show_view_count | body | boolean | 예 | — | 게시글 목록/상세에 조회수를 노출할지 여부 | +| secret_mode | body | string | 예 | `disabled`, `enabled`, `always` | 비밀글 설정 (disabled 사용 안 함 / enabled 작성자 선택 가능 / always 전 글 비밀글 강제) | +| use_comment | body | boolean | 예 | — | 댓글 기능 사용 여부 | +| use_reply | body | boolean | 예 | — | 게시글 답변(원글에 대한 답글) 기능 사용 여부 | +| use_report | body | boolean | 예 | — | 게시글/댓글 신고 기능 사용 여부 | +| comment_order | body | string | 예 | `ASC`, `DESC` | 댓글 정렬 순서 (ASC 오름차순 / DESC 내림차순) | +| new_display_hours | body | integer | 아니오 | min 1, max 720 | 신규(NEW) 표시를 유지할 기간 (시간 단위, 최대 720시간=30일) | +| min_title_length | body | integer | 아니오 | min 0, max 200 | 게시글 제목 최소 글자 수 | +| max_title_length | body | integer | 아니오 | min 1, max 1000 | 게시글 제목 최대 글자 수 | +| min_content_length | body | integer | 아니오 | min 0, max 10000 | 게시글 본문 최소 글자 수 | +| max_content_length | body | integer | 아니오 | min 1, max 100000 | 게시글 본문 최대 글자 수 | +| min_comment_length | body | integer | 아니오 | min 0, max 1000 | 댓글 최소 글자 수 | +| max_comment_length | body | integer | 아니오 | min 1, max 10000 | 댓글 최대 글자 수 | +| use_file_upload | body | boolean | 예 | — | 파일 첨부 기능 사용 여부 (true일 때 allowed_extensions 최소 1개 필수) | +| max_file_size | body | integer | 아니오 | min 1, max 200 | 첨부파일 1개당 최대 크기 (MB 단위) | +| max_file_count | body | integer | 아니오 | min 1, max 20 | 게시글 1건당 첨부할 수 있는 최대 파일 개수 | +| allowed_extensions | body | array | 아니오 | min 1 | 업로드 허용 확장자 목록 (예: jpg, png, pdf — 첨부 사용 시 최소 1개 필수) | +| board_manager_ids | body | array | 예 | min 1 | board manager 식별자 배열 | +| board_step_ids | body | array | 아니오 | — | board step 식별자 배열 | +| permissions | body | array | 예 | — | 게시판별 세부 권한 매트릭스 (권한 키별 mode/roles — 각 권한을 all 또는 특정 역할에 부여) | +| max_reply_depth | body | integer | 아니오 | min 1, max 10 | 답변글 최대 중첩 깊이 | +| max_comment_depth | body | integer | 아니오 | min 0, max 10 | 대댓글 최대 중첩 깊이 | +| notify_admin_on_post | body | boolean | 예 | — | 게시글 등록 시 관리자에게 이메일 알림 발송 여부 | +| notify_author | body | boolean | 예 | — | 댓글·대댓글·답변글·관리자 처리 발생 시 작성자에게 이메일 알림 발송 여부 | +| blocked_keywords | body | array | 아니오 | — | 게시글/댓글 작성 시 차단할 금지어 목록 (각 항목 최대 100자, 쉼표 구분 문자열도 허용) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.board.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판 설정을 수정합니다. `auth:sanctum` + `sirsoft-board.boards.update` 권한이 필요하며, `UpdateBoardRequest` 로 검증된 값으로 이름/설명/제한값/권한 매트릭스 등을 갱신합니다. 슬러그와 타입은 생성 시 고정되므로 이 요청으로는 변경 대상이 아니며, 성공 시 갱신된 게시판 리소스를 반환합니다. + + +### POST /api/modules/sirsoft-board/admin/boards/{board}/add-to-menu + +- **라우트명**: `api.modules.sirsoft-board.admin.boards.add-to-menu` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardController@addToAdminMenu` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.boards.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| board | path | string | 예 | — | 대상 board의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 해당 게시판을 관리자 메뉴에 등록합니다. `auth:sanctum` + `sirsoft-board.boards.update` 권한이 필요하며, 관리자 사이드바에서 게시판별 관리 메뉴를 바로 진입할 수 있도록 메뉴 항목을 생성합니다. 이미 등록된 게시판이면 `MenuAlreadyExistsException` 이 발생해 중복 등록을 막습니다. + + +### GET /api/modules/sirsoft-board/boards + +- **라우트명**: `api.modules.sirsoft-board.boards.index` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\BoardController@index` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `12` | 기본 키 (내부 식별자) | +| name | string | `API 문서 샘플 게시판` | 게시판명 (다국어 JSON) | +| slug | string | `apidoc-sample-board` | 게시판 슬러그 (URL/테이블명) | +| description | string | `` | 게시판 설명 (다국어 JSON) | +| posts_count | integer | `0` | posts 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 사용자 화면용 활성 게시판 목록을 경량으로 조회합니다. 전체 게시판 목록 페이지에서 사용하며, `id`/`name`/`slug`/`description`/`posts_count` 만 반환합니다. `limit` 파라미터(0~10, 기본 0)를 주면 게시판별 최신글도 함께 담아주며(답변글·삭제·블라인드 제외), 활성화된 게시판만 노출됩니다. + + +### GET /api/modules/sirsoft-board/boards/board-menu + +- **라우트명**: `api.modules.sirsoft-board.boards.board-menu` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\BoardController@boardMenu` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | string | `테스트 게시판` | 게시판명 (다국어 JSON) | +| slug | string | `test` | 게시판 슬러그 (URL/테이블명) | + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 헤더/네비게이션 메뉴에 노출할 게시판 목록을 최소 필드(`id`/`name`/`slug`)로 반환합니다. 활성 게시판만 오래된 순(created_at ASC)으로 정렬해 캐시에서 제공하는 경량 조회 API입니다. + + +### GET /api/modules/sirsoft-board/boards/popular + +- **라우트명**: `api.modules.sirsoft-board.boards.popular` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\BoardController@popular` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + + + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 조회수가 많은 인기 게시글 목록을 반환합니다. 홈/사이드바 위젯용 경량 조회로, `period`(today·week·month·year, 기본 week; `all` 은 하위 호환으로 year 로 매핑)로 기간을, `limit`(기본 20, 최대 50)으로 개수를 조절합니다. 결과는 캐시에서 제공됩니다. + + +### GET /api/modules/sirsoft-board/boards/popular-boards + +- **라우트명**: `api.modules.sirsoft-board.boards.popular-boards` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\BoardController@popularBoards` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `6` | 기본 키 (내부 식별자) | +| name | string | `Q&A` | 게시판명 (다국어 JSON) | +| slug | string | `qna` | 게시판 슬러그 (URL/테이블명) | +| description | string | `궁금한 점을 질문하고 답변을 받으세요` | 게시판 설명 (다국어 JSON) | +| posts_count | integer | `40` | posts 개수 (집계) | + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 게시글 수가 많은 인기 게시판 목록을 반환합니다. 홈 화면 등에서 활발한 게시판을 노출하는 데 사용하며, `limit`(기본 4, 최대 20)으로 개수를 조절합니다. 결과는 캐시에서 제공됩니다. + + +### GET /api/modules/sirsoft-board/boards/posts/recent + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.recent` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\BoardController@recentPosts` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `237` | 기본 키 (내부 식별자) | +| board_slug | string | `apidoc-sample-board` | 게시글이 속한 게시판의 슬러그 (게시글 URL 구성용) | +| board_name | string | `API 문서 샘플 게시판` | 게시글이 속한 게시판의 다국어 이름 | +| title | string | `API 문서 샘플 게시글` | 제목 | +| author_name | string | `API 문서 샘플 사용자` | 작성자 표시 이름 (회원은 회원명, 비회원은 입력한 이름) | +| created_at | string | `2026-07-07 09:34:50` | 생성 일시 | +| created_at_formatted | string | `방금 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| view_count | integer | `42` | view 개수 (집계) | +| comment_count | integer | `0` | comment 개수 (집계) | +| is_secret | boolean | `false` | secret 여부 | +| is_new | boolean | `true` | new 여부 | + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 모든 게시판을 통합한 최근 게시글 목록을 반환합니다. 홈 화면의 최신글 위젯 등에 사용하며, `limit`(기본 5, 최대 20)으로 개수를 조절합니다. 각 항목에는 게시판 슬러그/이름과 상대 시간(`created_at_formatted`), 신규 여부(`is_new`)가 함께 담기고, 결과는 캐시에서 제공됩니다. + + +### GET /api/modules/sirsoft-board/boards/stats + +- **라우트명**: `api.modules.sirsoft-board.boards.stats` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\BoardController@stats` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| users | integer | `156` | 전체 회원 수 | +| boards | integer | `10` | 활성 게시판 수 | +| posts | integer | `116` | 전체 게시글 수 | +| comments | integer | `321` | 전체 댓글 수 | + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 게시판 관련 집계 통계를 반환합니다. 홈 화면의 통계 카드 등에 사용하며, 회원 수·활성 게시판 수·전체 게시글 수·전체 댓글 수를 담아 캐시에서 제공하는 경량 조회 API입니다. + + +### GET /api/modules/sirsoft-board/boards/{slug} + +- **라우트명**: `api.modules.sirsoft-board.boards.show` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\BoardController@show` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `12` | 기본 키 (내부 식별자) | +| name | string | `API 문서 샘플 게시판` | 게시판명 (다국어 JSON) | +| slug | string | `apidoc-sample-board` | 게시판 슬러그 (URL/테이블명) | +| is_active | boolean | `true` | active 여부 | +| type | string | `basic` | 게시판 타입 (basic, gallery, card 등) | +| description | string | `` | 게시판 설명 (다국어 JSON) | +| per_page | integer | `20` | 페이지당 게시글 수 (PC) | +| per_page_mobile | integer | `15` | 페이지당 게시글 수 (Mobile) | +| order_by | string | `created_at` | 정렬 기준 (created_at, view_count, title, author) | +| order_direction | string | `DESC` | 정렬 방향 (ASC, DESC) | +| categories | array | `[]` | 분류 목록 (배열) | +| show_view_count | boolean | `true` | 조회수 노출 | +| secret_mode | string | `disabled` | 비밀글 설정 (disabled: 사용안함, enabled: 사용함, always: 고정) | +| use_comment | boolean | `true` | 댓글 기능 사용 | +| use_reply | boolean | `true` | 게시글 답변 기능 사용 (댓글에 대한 답글 아님) | +| max_reply_depth | integer | `5` | 답변글 최대 깊이 (1~5) | +| use_report | boolean | `true` | 게시글/댓글 신고 기능 사용 | +| comment_order | string | `ASC` | 댓글 정렬 순서 (ASC: 오름차순, DESC: 내림차순) | +| max_comment_depth | integer | `10` | 대댓글 최대 깊이 (1~10) | +| min_title_length | integer | `2` | 최소 제목 글자 수 | +| max_title_length | integer | `200` | 최대 제목 글자 수 | +| min_content_length | integer | `10` | 최소 게시글 글자 수 | +| max_content_length | integer | `10000` | 최대 게시글 글자 수 | +| min_comment_length | integer | `2` | 최소 댓글 글자 수 | +| max_comment_length | integer | `1000` | 최대 댓글 글자 수 | +| blocked_keywords | array | `[]` | 금지어 목록 (배열) | +| use_file_upload | boolean | `true` | 파일 업로드 사용 | +| max_file_size | integer | `10` | 최대 파일 크기 (MB) | +| max_file_count | integer | `5` | max file 개수 (집계) | +| allowed_extensions | array | `["jpg","jpeg","png","gif","pdf","zip"]` | 허용 확장자 배열 | +| add_to_menu | null | `null` | 관리자 메뉴 등록 여부 (폼 요청에서만 채워지는 토글 초기값 — 조회 응답에서는 항상 null) | +| new_display_hours | integer | `24` | 신규 게시글 표시 기간 (시간 단위) | +| board_managers | array | `[]` | 게시판 관리자로 지정된 사용자 목록 (manager 역할 사용자의 uuid/name/email — 역할 기반 파생) | +| board_steps | array | `[]` | 게시판 승인/처리 담당자(스텝)로 지정된 사용자 목록 (step 역할 사용자의 uuid/name/email — 역할 기반 파생) | +| board_manager_ids | array | `[]` | board manager 식별자 배열 (연관 리소스 참조) | +| board_step_ids | array | `[]` | board step 식별자 배열 (연관 리소스 참조) | +| notify_author | boolean | `true` | 작성자 이메일 알림 (댓글, 대댓글, 답변글, 관리자 처리 시) | +| notify_admin_on_post | boolean | `true` | 관리자 이메일 알림 (게시글 등록 시) | +| created_at | string | `2026-07-07 09:34:50` | 생성 일시 | +| updated_at | string | `2026-07-07 09:34:50` | 최종 수정 일시 | +| permissions | object | `{"admin_posts_read":{"_key":"admin_posts_read","name":"ad…` | 연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) | +| category_post_counts | null | `null` | 분류별 게시글 개수 맵 (요청 시 조건부로만 채워지며, 미포함 시 null) | +| posts_count | integer | `0` | posts 개수 (집계) | +| 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 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 사용자 화면에서 특정 게시판의 상세 설정을 조회합니다. 게시판 목록/글쓰기 진입 시 게시판 헤더와 규칙(제목/본문 길이, 파일 업로드, 댓글 설정 등)을 렌더링하는 데 사용합니다. 스코프 권한 검사 없이 조회하되, 게시판이 없거나 비활성 상태면 존재 여부를 숨기기 위해 404를 반환합니다. + + +### GET /api/modules/sirsoft-board/boards/{slug}/attachment/{hash} + +- **라우트명**: `api.modules.sirsoft-board.boards.attachment.download` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\AttachmentController@download` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.attachments.download` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.attachments.download`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 해시로 지정한 첨부파일을 다운로드로 제공합니다(`Content-Disposition: attachment`). `auth:sanctum` + `sirsoft-board.{slug}.attachments.download` 권한이 필요하며, 이미지 포함 모든 파일을 다운로드 방식으로 스트리밍합니다. 이미지의 인라인 미리보기는 별도의 preview 엔드포인트를 사용하고, 삭제글 첨부 등 접근 차단 시 403을 반환합니다. + + +### GET /api/modules/sirsoft-board/boards/{slug}/attachment/{hash}/preview + +- **라우트명**: `api.modules.sirsoft-board.boards.attachment.preview` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\AttachmentController@preview` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 해시로 지정한 이미지 첨부파일을 인라인 미리보기로 제공합니다. 게시글 본문 내 이미지 표시용이라 비회원도 접근할 수 있으며(권한 체크 없음), 이미지가 아닌 파일 요청 시 400을 반환합니다. 응답에는 레이아웃 캐시 TTL(기본 24시간) 기반 캐싱 헤더가 포함되고, 삭제글 첨부 등 차단 대상은 403을 반환합니다. + + +### POST /api/modules/sirsoft-board/boards/{slug}/attachments + +- **라우트명**: `api.modules.sirsoft-board.boards.attachments.upload` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\AttachmentController@upload` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.attachments.upload` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| file | body | file | 예 | — | 업로드 파일 | +| post_id | body | integer | 아니오 | min 1 | post 식별자 | +| collection | body | string | 아니오 | max 100 | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) | +| temp_key | body | string | 아니오 | max 64 | 게시글 저장 전 임시 업로드를 묶는 키 (post_id가 없을 때 첨부를 임시로 그룹핑, 저장 시 게시글에 연결) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.attachment.upload_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.attachments.upload`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글 작성/수정 폼에서 단일 파일을 업로드합니다. `auth:sanctum` + `sirsoft-board.{slug}.attachments.upload` 권한이 필요하며, 게시판의 파일 업로드 설정이 꺼져 있으면 403을 반환합니다. 게시글 저장 전 임시 업로드를 위해 `temp_key` 로 묶어두거나 `post_id` 로 기존 글에 바로 연결할 수 있고, 성공 시 FileUploader 컴포넌트가 기대하는 `data.data` 형식으로 첨부 메타(해시·URL·순서 등)를 201로 반환합니다. + + +### PATCH /api/modules/sirsoft-board/boards/{slug}/attachments/reorder + +- **라우트명**: `api.modules.sirsoft-board.boards.attachments.reorder` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\AttachmentController@reorder` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.attachments.upload` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| order | body | array | 예 | min 1 | 첨부 순서 항목 배열 (각 항목은 {id, order} — 첨부 ID별 새 순서 값) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.attachment.reorder_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.attachments.upload`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글에 첨부된 파일들의 표시 순서를 재정렬합니다. `auth:sanctum` + `sirsoft-board.{slug}.attachments.upload` 권한이 필요하며, FileUploader 가 보낸 `[{id, order}]` 배열을 받아 각 첨부의 순서 값을 갱신합니다. + + +### DELETE /api/modules/sirsoft-board/boards/{slug}/attachments/{id} + +- **라우트명**: `api.modules.sirsoft-board.boards.attachments.destroy` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\AttachmentController@destroy` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.attachments.upload` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.attachments.upload`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 첨부파일을 삭제합니다. `auth:sanctum` + `sirsoft-board.{slug}.attachments.upload` 권한이 필요하며, 서비스의 `canDelete` 로 현재 사용자가 해당 첨부의 소유자(작성자)인지 확인한 뒤 삭제합니다. 권한이 없으면 403을 반환합니다. + + +### POST /api/modules/sirsoft-board/boards/{slug}/comments/{commentId}/reports + +- **라우트명**: `api.modules.sirsoft-board.boards.comments.reports.store` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\ReportController@storeCommentReport` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| commentId | path | string | 예 | — | 대상 comment의 식별자 | +| reason_type | body | string | 예 | — | 신고 사유 유형 (ReportReasonType Enum 값 중 하나 — 스팸/욕설 등) | +| reason_detail | body | string | 예 | min 1, max 1000 | 신고 사유 상세 설명 (1~1000자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 댓글을 신고합니다. 신고는 회원 전용이라 컨트롤러가 `AuthBaseController` 를 상속해 로그인 사용자만 호출할 수 있으며, 게시판의 신고 기능이 꺼져 있으면 403을 반환합니다. 본인 댓글 신고, 블라인드/삭제된 대상 신고는 차단되고, 이미 신고한 대상이면 409(중복)로 응답합니다. 신고 사유 유형(`reason_type`)과 상세(`reason_detail`)를 받아 신고를 접수하며 사용자 활동 로그를 남깁니다. + + +### POST /api/modules/sirsoft-board/boards/{slug}/comments/{commentId}/verify-password + +- **라우트명**: `api.modules.sirsoft-board.boards.comments.verify-password` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\CommentController@verifyPassword` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.comments.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| commentId | path | string | 예 | — | 대상 comment의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.comments.write`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 비회원이 작성한 댓글의 수정/삭제 권한을 확인하기 위해 비밀번호를 검증합니다. `auth:sanctum` + `sirsoft-board.{slug}.comments.write` 권한이 필요하며, 대상이 비회원 댓글이 아니면 400, 비밀번호가 틀리면 401을 반환합니다. 검증 성공 시 1시간 유효한 임시 토큰(`verification_token`)을 발급해 이후 수정/삭제 요청에서 비밀번호 재입력 없이 사용하도록 합니다. + + +### GET /api/modules/sirsoft-board/boards/{slug}/posts + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.index` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@index` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.posts.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `237` | 기본 키 (내부 식별자) | +| category | null | `null` | 게시글 분류(카테고리) 이름 (분류 미사용 게시판이거나 미지정 시 null) | +| author | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 작성자 사용자 객체 (uuid/name — author 관계 파생) | +| is_notice | boolean | `false` | notice 여부 | +| is_secret | boolean | `false` | secret 여부 | +| content_mode | string | `html` | 본문 편집 모드 (html: WYSIWYG/HTML, text: 일반 텍스트) | +| is_new | boolean | `true` | new 여부 | +| status | string | `published` | 게시 상태 (published 게시됨 / blinded 블라인드 처리 / deleted 삭제됨 — PostStatus Enum 값) | +| 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 | string | `/api/modules/sirsoft-board/boards/api…` | 썸네일 이미지 URL/경로 | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| is_reply | boolean | `false` | reply 여부 | +| created_at | string | `2026-07-07 09:34:50` | 생성 일시 | +| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| is_author | boolean | `true` | author 여부 | +| is_guest_post | boolean | `false` | guest post 여부 | +| slug | string | `apidoc-sample-board` | 게시판 슬러그 (URL/테이블명) | +| title | string | `API 문서 샘플 게시글` | 제목 | +| deleted_at | null | `null` | 소프트 삭제 일시 (미삭제 시 null) | +| content_preview | string | `API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.` | 목록용 본문 요약 (평문 앞 150자, 블라인드/비밀글은 원문 유출 방지를 위해 빈 문자열) | +| row_type | string | `normal` | 목록 행 유형 (notice 공지 / reply 답변글 / normal 일반글 — 표시 스타일·순번 처리 구분) | +| number | integer | `1` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| show_category | boolean | `false` | 게시판에 분류가 설정돼 있어 목록에 분류 컬럼을 노출할지 여부 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시판의 게시글 목록을 조회합니다. `auth:sanctum` + `sirsoft-board.{slug}.posts.read` 권한이 필요하며(공개 게시판은 게스트에게도 read 권한이 부여될 수 있음), 검색/카테고리/정렬 등 필터와 페이지네이션을 지원합니다. 성능을 위해 simplePaginate 로 조회하되 일반 게시글 총 건수는 캐시에서 별도로 채우며, manager 권한 보유자가 `del=1` 을 주면 삭제된 게시글까지 포함합니다. + + +### POST /api/modules/sirsoft-board/boards/{slug}/posts + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.store` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@store` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.posts.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.user_post.store_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.write`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글을 작성합니다. `auth:sanctum` + `sirsoft-board.{slug}.posts.write` 권한이 필요하며, `StorePostRequest` 로 검증된 제목/본문/카테고리 등을 받아 현재 로그인 사용자를 작성자로 저장합니다. 파일이 첨부되면 게시판의 업로드 허용 여부와 첨부 업로드 권한을 함께 확인하고, 게시판 `secret_mode` 에 따라 비밀글 설정을 강제(always)하거나 거부(disabled)합니다. 스팸 방지 쿨다운이 설정되어 있으면 작성 성공 후 쿨다운을 기록합니다. + + +### GET /api/modules/sirsoft-board/boards/{slug}/posts/form-data + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.form-data` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@getFormData` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.posts.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| title | string | `` | 제목 | +| content | string | `` | 본문 내용 | +| content_mode | string | `text` | 본문 편집 모드 (html: WYSIWYG/HTML, text: 일반 텍스트) | +| category | null | `null` | 게시글 분류(카테고리) 이름 (분류 미사용 게시판이거나 미지정 시 null) | +| is_notice | boolean | `false` | notice 여부 | +| is_secret | boolean | `false` | secret 여부 | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.write`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글 작성/수정 폼의 초기 입력값을 반환합니다. `auth:sanctum` + `sirsoft-board.{slug}.posts.write` 권한이 필요하며, 쿼리 파라미터로 모드가 분기됩니다: `post_id` 가 있으면 기존 글 값(수정 모드), `parent_id` 가 있으면 원글 제목을 기반으로 한 답변글 초기값(답변 모드), 둘 다 없으면 빈 기본값(생성 모드)입니다. 수정/답변 모드에서는 본인·관리자 여부와 비회원 글의 비밀번호/토큰 검증을 확인하며, 답변 대상이 블라인드/삭제 상태면 진입을 차단합니다. + + +### GET /api/modules/sirsoft-board/boards/{slug}/posts/form-meta + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.form-meta` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@getFormMeta` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.posts.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| board | object | `{"id":12,"name":"API 문서 샘플 게시판","slug":"apidoc-sample-boa…` | 글쓰기/수정 폼 렌더링용 게시판 메타 (id/name/slug/type + 게시판 설정과 현재 사용자 권한(user_abilities) 등) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.write`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글 작성/수정 폼 렌더링에 필요한 게시판 메타 정보를 반환합니다. `auth:sanctum` + `sirsoft-board.{slug}.posts.write` 권한이 필요하며, 게시판 설정과 사용자 권한(`user_abilities`)을 담습니다. `post_id` 가 있으면 수정 모드로 작성자/작성일/첨부 목록을 포함하되 회원 글은 본인 또는 관리자만, 비회원 글은 비밀번호/검증 토큰 확인을 요구합니다. `parent_id` 가 있으면 답변 모드로 원글 정보를 포함하며, 답변 기능이 꺼져 있거나 원글이 블라인드/삭제 상태면 차단합니다. + + +### DELETE /api/modules/sirsoft-board/boards/{slug}/posts/{id} + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.destroy` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@destroy` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.posts.write|sirsoft-board.{slug}.manager` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.write|sirsoft-board.{slug}.manager`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글을 삭제(소프트 삭제)합니다. `auth:sanctum` + `sirsoft-board.{slug}.posts.write` 또는 게시판 manager 권한이 필요합니다. 작성자 본인, 게시판 관리자(admin.manage/manager), 또는 비회원 글의 경우 검증 토큰·비밀번호 확인 중 하나를 만족해야 삭제할 수 있으며, 조건 미충족 시 403을 반환합니다. + + +### GET /api/modules/sirsoft-board/boards/{slug}/posts/{id} + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.show` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@show` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.posts.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `237` | 기본 키 (내부 식별자) | +| category | null | `null` | 게시글 분류(카테고리) 이름 (분류 미사용 게시판이거나 미지정 시 null) | +| author | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 작성자 사용자 객체 (uuid/name — author 관계 파생) | +| is_notice | boolean | `false` | notice 여부 | +| is_secret | boolean | `false` | secret 여부 | +| content_mode | string | `html` | 본문 편집 모드 (html: WYSIWYG/HTML, text: 일반 텍스트) | +| is_new | boolean | `true` | new 여부 | +| status | string | `published` | 게시 상태 (published 게시됨 / blinded 블라인드 처리 / deleted 삭제됨 — PostStatus Enum 값) | +| 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 | string | `/api/modules/sirsoft-board/boards/api…` | 썸네일 이미지 URL/경로 | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| is_reply | boolean | `false` | reply 여부 | +| created_at | string | `2026-07-07 09:34:50` | 생성 일시 | +| 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 | `a231747f-e82e-4cf2-9ae1-a261849dce40` | user 식별자 (연관 리소스 참조) | +| trigger_type | string | `user` | 상태 변경/삭제를 유발한 주체 (user 사용자 / admin 관리자 / report 신고 / system 시스템 / auto_hide 자동 블라인드 / cascade 연쇄 삭제 — TriggerType Enum) | +| updated_at | string | `2026-07-07 09:39:03` | 최종 수정 일시 | +| deleted_at | null | `null` | 소프트 삭제 일시 (미삭제 시 null) | +| ip_address | string | `127.0.0.1` | 요청/행위가 발생한 IP 주소 | +| action_logs | array | `[]` | 블라인드/복원/삭제 등 운영 처리 이력 (admin.manage 권한 보유자에게만 노출, action/reason/admin_name/created_at — 비권한자는 빈 배열/null) | +| board | object | `{"slug":"apidoc-sample-board","name":"API 문서 샘플 게시판","typ…` | 게시글이 속한 게시판 정보 (slug/name/type + 댓글·답변·신고·조회수 노출·깊이 등 렌더링에 필요한 설정과 신고 유형 목록) | +| navigation | null | `null` | 이전/다음 글 정보 (상세 API에서는 기본 null — 별도 navigation 엔드포인트로 비동기 로딩) | +| parent | null | `null` | 상위 항목 객체 (parent 관계 파생) | +| comments | array | `[{"id":760,"post_id":237,"parent_id":null,"content":"API …` | 이 게시글의 댓글 목록 (CommentResource 배열 — 대댓글 계층 포함) | +| attachments | array | `[{"id":155,"hash":"apidocsmpl1","original_filename":"apid…` | 이 게시글의 첨부파일 목록 (비밀글/삭제글은 권한에 따라 빈 배열 또는 cascade 항목만 노출) | +| replies | array | `[]` | 이 게시글에 달린 답변글 목록 (PostResource 배열 — use_reply 게시판에서만 로드) | +| 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 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글 상세를 조회합니다. `auth:sanctum` + `sirsoft-board.{slug}.posts.read` 권한이 필요하며, 댓글/첨부/답글을 함께 로드하고 조회수를 (중복 방지 캐시로) 1회 증가시킵니다. 삭제된 게시글은 manager 권한이 있어야 열람 가능하고, 비밀글의 본문 노출 여부와 신고 이력 표시는 로그인 사용자 기준으로 결정됩니다(비로그인은 신고 불가로 모두 false). manager 가 `del_cmt=1` 을 주면 삭제된 댓글도 포함합니다. + + +### PUT /api/modules/sirsoft-board/boards/{slug}/posts/{id} + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.update` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@update` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.posts.write|sirsoft-board.{slug}.manager` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.user_post.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.write|sirsoft-board.{slug}.manager`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글을 수정합니다. `auth:sanctum` + `sirsoft-board.{slug}.posts.write` 또는 게시판 manager 권한이 필요하며, `UpdatePostRequest` 로 검증된 값으로 갱신합니다. 작성자 본인, 게시판 관리자, 또는 비회원 글의 검증 토큰·비밀번호 확인 중 하나를 만족해야 수정할 수 있고, 조건 미충족 시 403을 반환합니다. + + +### GET /api/modules/sirsoft-board/boards/{slug}/posts/{id}/navigation + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.navigation` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@navigation` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.posts.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| prev | null | `null` | 정렬 기준상 이전 글 정보 (없거나 공지·답글·조회 실패 시 null) | +| next | null | `null` | 정렬 기준상 다음 글 정보 (없거나 공지·답글·조회 실패 시 null) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글 상세 화면의 이전/다음 글 정보를 조회합니다(상세 API에서 분리한 비동기 로딩용). `auth:sanctum` + `sirsoft-board.{slug}.posts.read` 권한이 필요하며, 게시판 정렬 설정과 현재 글의 카테고리를 기준으로 인접 글을 계산합니다. 공지글·답글·존재하지 않는 글이거나 내부 조회 실패 시에도 500 대신 `{prev: null, next: null}` 로 안전하게 응답하며, manager 가 `del=1` 을 주면 삭제된 글까지 후보에 포함합니다. + + +### POST /api/modules/sirsoft-board/boards/{slug}/posts/{id}/verify-password + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.verify-password` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@verifyPassword` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.posts.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| password | body | string | 예 | — | 비밀번호 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 비밀글 열람을 위해 비밀번호를 검증합니다. `auth:sanctum` + `sirsoft-board.{slug}.posts.read` 권한이 필요하며, 주로 비회원 비밀글의 내용을 확인할 때 사용합니다. 검증에 성공하면 `password_verified` 플래그가 설정되어 게시글 본문과 첨부파일을 포함한 상세를 반환하고, 실패 시 서비스가 지정한 에러 키/코드로 응답합니다. + + +### POST /api/modules/sirsoft-board/boards/{slug}/posts/{id}/verify-password-for-modify + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.verify-password-for-modify` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@verifyPasswordForModify` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.posts.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| password | body | string | 예 | — | 비밀번호 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.write`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글 수정/삭제 전 권한 확인을 위해 비밀번호를 검증합니다. `auth:sanctum` + `sirsoft-board.{slug}.posts.write` 권한이 필요하며, 주로 비회원 글의 소유 확인에 사용합니다. 검증 성공 시 32자 임시 토큰(`verification_token`)과 만료 시각을 발급하여, 이후 수정/삭제 요청에서 비밀번호를 다시 보내지 않고 토큰으로 권한을 증명하게 합니다. + + +### GET /api/modules/sirsoft-board/boards/{slug}/posts/{postId}/comments + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.comments.index` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\CommentController@index` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.comments.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| postId | path | string | 예 | — | 대상 post의 식별자 | + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `760` | 기본 키 (내부 식별자) | +| post_id | integer | `237` | post 식별자 (연관 리소스 참조) | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| content | string | `API 문서 샘플 댓글입니다.` | 본문 내용 | +| author | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 작성자 사용자 객체 (uuid/name — author 관계 파생) | +| is_secret | boolean | `false` | secret 여부 | +| status | string | `published` | 게시 상태 (published 게시됨 / blinded 블라인드 처리 / deleted 삭제됨 — PostStatus Enum 값) | +| status_label | string | `게시됨` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| replies_count | integer | `0` | replies 개수 (집계) | +| created_at | string | `2026-07-07 09:34:50` | 생성 일시 | +| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| updated_at | string | `2026-07-07 09:34:50` | 최종 수정 일시 | +| deleted_at | null | `null` | 소프트 삭제 일시 (미삭제 시 null) | +| is_cascade_deleted | boolean | `false` | cascade deleted 여부 | +| ip_address | null | `null` | 요청/행위가 발생한 IP 주소 | +| action_logs | array | `[]` | 블라인드/복원/삭제 등 운영 처리 이력 (admin.manage 권한 보유자에게만 노출, action/reason/admin_name/created_at — 비권한자는 빈 배열/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_write":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.comments.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글의 댓글 목록을 조회합니다. `auth:sanctum` + `sirsoft-board.{slug}.comments.read` 권한이 필요하며, 게시판의 댓글 기능이 꺼져 있으면 403을 반환합니다. 대댓글 계층(`depth`, `replies_count`)을 포함하며, 각 항목에 현재 사용자의 작성자 여부·소유 여부·신고 이력 등이 메타로 담깁니다. + + +### POST /api/modules/sirsoft-board/boards/{slug}/posts/{postId}/comments + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.comments.store` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\CommentController@store` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.comments.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| postId | path | string | 예 | — | 대상 post의 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.comment.store_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.comments.write`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 게시글에 댓글(또는 대댓글)을 작성합니다. `auth:sanctum` + `sirsoft-board.{slug}.comments.write` 권한이 필요하며, 게시판의 댓글 기능이 꺼져 있으면 403을 반환합니다. 현재 로그인 사용자를 작성자로 저장하고 요청 IP를 기록하며, 스팸 방지 쿨다운이 설정되어 있으면 작성 성공 후 쿨다운을 기록합니다. + + +### DELETE /api/modules/sirsoft-board/boards/{slug}/posts/{postId}/comments/{commentId} + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.comments.destroy` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\CommentController@destroy` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.comments.write|sirsoft-board.{slug}.manager` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| postId | path | string | 예 | — | 대상 post의 식별자 | +| commentId | path | string | 예 | — | 대상 comment의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.comments.write|sirsoft-board.{slug}.manager`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 댓글을 삭제합니다. `auth:sanctum` + `sirsoft-board.{slug}.comments.write` 또는 게시판 manager 권한이 필요하며, 게시판의 댓글 기능이 꺼져 있으면 403을 반환합니다. 서비스의 `canDelete` 가 작성자 본인·게시판 관리자·비회원 댓글의 비밀번호 확인 여부를 판정하며, 조건 미충족 시 403을 반환합니다. + + +### PUT /api/modules/sirsoft-board/boards/{slug}/posts/{postId}/comments/{commentId} + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.comments.update` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\CommentController@update` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-board.{slug}.comments.write|sirsoft-board.{slug}.manager` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| postId | path | string | 예 | — | 대상 post의 식별자 | +| commentId | path | string | 예 | — | 대상 comment의 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.comment.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.comments.write|sirsoft-board.{slug}.manager`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 댓글을 수정합니다. `auth:sanctum` + `sirsoft-board.{slug}.comments.write` 또는 게시판 manager 권한이 필요하며, 게시판의 댓글 기능이 꺼져 있으면 403을 반환합니다. 서비스의 `canUpdate` 가 작성자 본인·게시판 관리자·비회원 댓글의 비밀번호 확인 여부를 판정하며, 검증용 `password` 는 저장에서 제외됩니다. + + +### POST /api/modules/sirsoft-board/boards/{slug}/posts/{postId}/reports + +- **라우트명**: `api.modules.sirsoft-board.boards.posts.reports.store` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\ReportController@storePostReport` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| postId | path | string | 예 | — | 대상 post의 식별자 | +| reason_type | body | string | 예 | — | 신고 사유 유형 (ReportReasonType Enum 값 중 하나 — 스팸/욕설 등) | +| reason_detail | body | string | 예 | min 1, max 1000 | 신고 사유 상세 설명 (1~1000자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 게시글을 신고합니다. 신고는 회원 전용이라 컨트롤러가 `AuthBaseController` 를 상속해 로그인 사용자만 호출할 수 있으며, 게시판의 신고 기능이 꺼져 있으면 403을 반환합니다. 본인 글 신고, 블라인드/삭제된 대상 신고는 차단되고, 이미 신고한 대상이면 409(중복)로 응답합니다. 신고 사유 유형(`reason_type`)과 상세(`reason_detail`)를 받아 신고를 접수하며 사용자 활동 로그를 남깁니다. + + diff --git a/modules/_bundled/sirsoft-board/docs/api/dashboard.md b/modules/_bundled/sirsoft-board/docs/api/dashboard.md new file mode 100644 index 00000000..ce2469d1 --- /dev/null +++ b/modules/_bundled/sirsoft-board/docs/api/dashboard.md @@ -0,0 +1,153 @@ +# Dashboard API 레퍼런스 + +> **소유**: module `sirsoft-board` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Dashboard 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-board/admin/dashboard/overview + +- **라우트명**: `api.modules.sirsoft-board.admin.dashboard.overview` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\DashboardController@overview` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| today_posts | integer | `0` | 오늘 등록된 새 게시글 수입니다. board_stats 집계 테이블의 오늘 행에서 읽으며, 행이 없으면 0 을 반환합니다(최대 1시간 지연). | +| today_comments | integer | `0` | 오늘 등록된 새 댓글 수입니다. board_stats 집계 테이블의 오늘 행에서 읽으며, 행이 없으면 0 을 반환합니다(최대 1시간 지연). | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 관리자 대시보드 위젯에 표시할 오늘의 게시판 현황을 반환합니다. `auth:sanctum` 인증이 필요하며(대시보드 진입은 코어 `core.dashboard.read` 가드로 보호), 오늘 등록된 새 글 수(today_posts)와 새 댓글 수(today_comments)를 집계하여 반환합니다. + + +### GET /api/modules/sirsoft-board/admin/dashboard/pending-reports + +- **라우트명**: `api.modules.sirsoft-board.admin.dashboard.pending-reports` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\DashboardController@pendingReports` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| limit | query | integer | 아니오 | min 1, max 50 | 반환할 최대 항목 수 | + +**응답 필드** (`data` 내부) + +_단건 응답: `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` | 전체 개수 (집계) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 전체 게시판의 미처리 신고 목록과 총 건수를 대시보드 위젯에 표시하기 위해 반환합니다. `auth:sanctum` 인증이 필요하며(대시보드 진입은 코어 `core.dashboard.read` 가드로 보호), `limit`(1~50, 기본 5)으로 표시 건수를 제어합니다. 응답은 미처리 신고 항목(items)과 전체 미처리 건수(total)를 포함합니다. + + +### GET /api/modules/sirsoft-board/admin/dashboard/post-graph + +- **라우트명**: `api.modules.sirsoft-board.admin.dashboard.post-graph` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\DashboardController@postGraph` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| days | array | `[{"date":"2026-07-01","post_count":0,"comment_count":0},{…` | 최근 7일간 일자별 집계 막대 배열입니다. 각 원소는 날짜(date)와 해당 일의 게시글 수(post_count)/댓글 수(comment_count)를 담으며, 집계 행이 없는 날은 0 으로 채워집니다. | +| total_posts | integer | `0` | 이번 7일 기간의 게시글 합계입니다. days 의 post_count 를 합산한 값입니다. | +| total_comments | integer | `0` | 이번 7일 기간의 댓글 합계입니다. days 의 comment_count 를 합산한 값입니다. | +| posts_change | null | `null` | 직전 동일 7일 기간 대비 게시글 증감율(%)입니다. 소수점 첫째 자리까지 계산하며, 직전 기간 합이 0 이면 비교 기준이 없으므로 null 을 반환합니다(화면에서 '—' 폴백). | +| comments_change | null | `null` | 직전 동일 7일 기간 대비 댓글 증감율(%)입니다. 소수점 첫째 자리까지 계산하며, 직전 기간 합이 0 이면 비교 기준이 없으므로 null 을 반환합니다(화면에서 '—' 폴백). | +| updated_at | null | `null` | 최종 수정 일시 | +| updated_at_display | string | `` | 집계 행의 최종 갱신 시각(updated_at)을 모듈 날짜 표시 형식(display.date_display_format 설정)으로 포맷한 표시용 문자열입니다. 집계 행이 없으면 빈 문자열입니다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 대시보드 위젯의 추세 그래프에 표시할 최근 7일간 게시글/댓글 추이를 반환합니다. `auth:sanctum` 인증이 필요하며(대시보드 진입은 코어 `core.dashboard.read` 가드로 보호), 일자별 막대(days), 기간 합계(total_posts/total_comments), 이전 기간 대비 변화율(posts_change/comments_change), 갱신 시각(updated_at)을 포함합니다. + + +### GET /api/modules/sirsoft-board/admin/dashboard/recent-posts + +- **라우트명**: `api.modules.sirsoft-board.admin.dashboard.recent-posts` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\DashboardController@recentPosts` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| limit | query | integer | 아니오 | min 1, max 50 | 반환할 최대 항목 수 | + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `237` | 기본 키 (내부 식별자) | +| board_slug | string | `apidoc-sample-board` | 게시글이 속한 게시판의 슬러그입니다. 대시보드에서 해당 게시판으로 이동할 때 식별자로 사용됩니다. | +| board_name | string | `API 문서 샘플 게시판` | 게시글이 속한 게시판의 현재 로케일 표시명입니다(getLocalizedName). | +| title | string | `API 문서 샘플 게시글` | 제목 | +| author_name | string | `API 문서 샘플 사용자` | 작성자 이름입니다. 회원 게시글은 연결된 사용자 이름을, 비회원 게시글은 작성 시 입력한 이름(author_name)을 사용합니다. | +| comments_count | integer | `0` | comments 개수 (집계) | +| created_at | string | `4시간 전` | 생성 일시 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 전체 게시판에서 최신 게시글을 대시보드 위젯에 표시하기 위해 반환합니다. `auth:sanctum` 인증이 필요하며(대시보드 진입은 코어 `core.dashboard.read` 가드로 보호), `limit`(1~50, 기본 5)으로 표시 건수를 제어합니다. 각 항목은 게시판(board_slug/board_name), 제목, 작성자, 댓글 수, 작성 시각(상대 시간 표기)을 포함합니다. + + diff --git a/modules/_bundled/sirsoft-board/docs/api/my-comments.md b/modules/_bundled/sirsoft-board/docs/api/my-comments.md new file mode 100644 index 00000000..8c282591 --- /dev/null +++ b/modules/_bundled/sirsoft-board/docs/api/my-comments.md @@ -0,0 +1,55 @@ +# My Comments API 레퍼런스 + +> **소유**: module `sirsoft-board` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 My Comments 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-board/me/my-comments + +- **라우트명**: `api.modules.sirsoft-board.me.my-comments.index` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\UserActivityController@myComments` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `760` | 기본 키 (내부 식별자) | +| board_slug | string | `apidoc-sample-board` | 댓글이 달린 게시글이 속한 게시판의 슬러그(URL 식별자)입니다. 게시판 링크 구성에 사용합니다. | +| board_name | string | `API 문서 샘플 게시판` | 댓글이 달린 게시글이 속한 게시판의 표시 이름입니다. 현재 로케일에 맞는 다국어 이름(`getLocalizedName()`)이 적용됩니다. | +| post_title | string | `API 문서 샘플 게시글` | 댓글이 달린 원 게시글의 제목입니다. 목록에서 어느 글에 남긴 댓글인지 식별하는 데 사용합니다. | +| post_id_val | integer | `237` | 댓글이 달린 원 게시글의 ID입니다. 원 게시글 상세로 이동하는 링크 구성에 사용합니다. | +| content | string | `API 문서 샘플 댓글입니다.` | 본문 내용 | +| created_at | string | `2026-07-07 09:34:50` | 생성 일시 | +| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 로그인한 회원 본인이 작성한 댓글 목록을 마이페이지에 표시하기 위해 반환하며, 각 항목에 댓글 내용과 함께 원 게시글 제목·게시판 정보를 포함합니다. `auth:sanctum` 인증이 필요한 회원 전용 엔드포인트로, 대상은 항상 인증된 본인(`Auth::id()`)입니다. `board_slug`·`search`·`sort`(기본 latest) 필터와 `per_page`(1~100, 기본 20) 페이지네이션을 지원합니다. + + diff --git a/modules/_bundled/sirsoft-board/docs/api/reports.md b/modules/_bundled/sirsoft-board/docs/api/reports.md new file mode 100644 index 00000000..64d86e6a --- /dev/null +++ b/modules/_bundled/sirsoft-board/docs/api/reports.md @@ -0,0 +1,309 @@ +# Reports API 레퍼런스 + +> **소유**: module `sirsoft-board` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Reports 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-board/admin/reports + +- **라우트명**: `api.modules.sirsoft-board.admin.reports.index` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\ReportController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.reports.view` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| filters | query | array | 아니오 | — | 추가 필터 조건 맵 (필드별 조건) | +| status | query | string | 아니오 | — | 상태 필터 (해당 상태의 항목만 조회) | +| target_type | query | string | 아니오 | — | 신고 대상 타입 필터 (`post`/`comment`). 단일 문자열 또는 배열로 여러 타입을 전달할 수 있습니다. | +| target_status | query | string | 아니오 | — | 신고 대상 콘텐츠의 현재 상태 필터 (게시글/댓글의 status 기준). 단일 문자열 또는 배열로 전달하며 `all`은 무시됩니다. | +| board_id | query | integer | 아니오 | — | board 식별자 | +| reported_at_from | query | string | 아니오 | — | 신고 접수 기간 시작일 (`YYYY-MM-DD`). 마지막 신고일시(`last_reported_at`) 기준으로 이 날짜 00:00:00 이후 건만 조회합니다. | +| reported_at_to | query | string | 아니오 | — | 신고 접수 기간 종료일 (`YYYY-MM-DD`). 마지막 신고일시(`last_reported_at`) 기준으로 이 날짜 23:59:59 이전 건만 조회합니다. | +| sort_by | query | string | 아니오 | — | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| per_page | query | integer | 아니오 | — | 페이지당 항목 수 | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `7` | 기본 키 (내부 식별자) | +| board_id | integer | `6` | 게시판 ID (게시판 삭제 시 NULL) | +| board | object | `{"id":6,"name":"Q&A","slug":"qna","title":"파일 업로드 오류","cu…` | 신고 대상이 속한 게시판 정보 (id/name/slug/대상 제목/현재 상태). 게시판 삭제 시 관계 대신 첫 신고 로그의 스냅샷 값으로 폴백합니다. | +| target_type | string | `comment` | 신고 대상 타입 (post, comment) | +| target_type_label | string | `댓글` | `target_type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| target_id | integer | `282` | 신고 대상 ID (동적 테이블의 ID) | +| post_id | integer | `99` | post 식별자 (연관 리소스 참조) | +| content | null | `null` | 본문 내용 | +| content_mode | string | `text` | 대상 본문의 형식 모드 (`text`/`html` 등). 목록에서는 미리보기 표시 방식을 결정하며 스냅샷이 없으면 `text`로 기본 설정됩니다. | +| content_preview | string | `저도 비슷한 경험이 있어요.` | 대상 본문의 미리보기 (앞 100자로 잘린 발췌). 목록 응답에서만 채워지고 상세 응답에서는 null 입니다. | +| author | object | `{"uuid":"a1e0a91a-fba6-491c-a53e-7285a5686857","name":"관리…` | 작성자 사용자 객체 (uuid/name — author 관계 파생) | +| reporter | object | `{"uuid":"a1e0a91a-fba6-491c-a53e-7285a5686857","name":"관리…` | 대표 신고자 정보 (uuid/name/email/is_guest). 첫 번째 신고 로그에서 추출하며, 비회원 신고 시 게스트로 표시됩니다. | +| reason_type | string | `abuse` | 대표 신고 사유 코드 (첫 번째 신고 로그의 사유 Enum 값 — 예: abuse, spam). | +| reason_type_label | string | `욕설/비방` | `reason_type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| status | string | `review` | 신고 상태 (pending, review, rejected, suspended) | +| status_label | string | `검토` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| status_variant | string | `info` | 상태 표시 색상/스타일 변형 키 (상태 Enum variant() 산물 — UI 배지용) | +| processor | object | `{"uuid":"a1e0a91a-fba6-491c-a53e-7285a5686857","name":"관리자"}` | 신고를 처리한 관리자 정보 (uuid/name). 처리자가 지정되지 않은 경우 null 입니다. | +| processed_at | string | `2026-06-01 09:35:42` | processed 일시 | +| metadata | null | `null` | 메타데이터 (IP, User Agent 등) | +| report_count | integer | `1` | report 개수 (집계) | +| last_reported_at | string | `2026-06-02 09:35:42` | last reported 일시 | +| is_reactivated | boolean | `false` | reactivated 여부 | +| target_status | string | `published` | 신고 대상 콘텐츠(게시글/댓글)의 현재 상태. 대상 테이블의 status 컬럼을 서브쿼리로 조인한 값입니다 (예: published, blinded). | +| target_trigger_type | string | `admin` | 신고 대상의 현재 상태를 유발한 트리거 유형. 대상 테이블의 trigger_type 값으로, 블라인드 처리가 자동(auto_hide)/관리자 수동(admin) 중 무엇에 의한 것인지 구분합니다. | +| target_status_label | string | `게시중` | `target_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| created_at | string | `2026-06-04 09:35:42` | 생성 일시 | +| updated_at | string | `2026-06-04 09:35:42` | 최종 수정 일시 | +| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_view":true,"can_manage":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.view`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 신고 관리 화면의 목록을 조회합니다. `auth:sanctum` 인증과 `sirsoft-board.reports.view` 권한이 필요합니다. 동일 대상(게시글/댓글)에 대한 여러 신고는 그룹화되어 최초 신고 1건만 목록에 노출되며, `filters`/`status`/`target_type`/`target_status`/`board_id`/기간(`reported_at_from`~`reported_at_to`)으로 필터링하고 `sort_by`/`sort_order`로 정렬합니다. `per_page`는 10~20 범위로 강제 제한되며, 응답에는 상태별 통계와 사용자 권한 정보가 함께 포함됩니다. + + +### PATCH /api/modules/sirsoft-board/admin/reports/bulk-status + +- **라우트명**: `api.modules.sirsoft-board.admin.reports.bulk-status` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\ReportController@bulkUpdateStatus` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.reports.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| status | body | string | 예 | — | 일괄 전환할 신고 상태 (`ReportStatus` 허용값: pending/review/rejected/suspended 등). 지정한 모든 신고를 이 상태로 변경합니다. | +| process_note | body | string | 아니오 | max 1000 | 처리 메모 (최대 1000자). 상태 변경 사유나 조치 내용을 처리 이력에 함께 기록합니다. | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.manage`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 여러 신고의 상태를 한 번에 변경합니다. `auth:sanctum` 인증과 `sirsoft-board.reports.manage` 권한이 필요합니다. `ids`로 지정한 신고들만 대상으로 하며(그룹 확장 없음) `status`로 지정한 상태로 일괄 전환하고, 선택적으로 `process_note`(최대 1000자)를 처리 메모로 남깁니다. 응답은 실제 변경된 건수(`affected_count`), 대상 콘텐츠 복구 건수(`restored_count`), 수동 블라인드 복구 건수(`manual_blind_restored`)와 안내 메시지를 반환합니다. + + +### POST /api/modules/sirsoft-board/admin/reports/status-counts + +- **라우트명**: `api.modules.sirsoft-board.admin.reports.status-counts` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\ReportController@getStatusCounts` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.reports.view` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| target_status | body | string | 아니오 | — | 집계 기준이 되는 전환 대상 상태 (`ReportStatus` 허용값). 지정 시 해당 상태로의 일괄 전환을 가정한 상태별 건수 요약을 계산합니다. | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.view`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 선택된 신고들의 상태별 건수를 집계하여 반환합니다. `auth:sanctum` 인증과 `sirsoft-board.reports.view` 권한이 필요합니다. 대량 상태 변경을 실행하기 전에 사용자에게 선택한 신고들의 상태 분포를 미리 보여주기 위한 조회용 API로, `ids` 배열(최소 1개)과 선택적 `target_status`를 받아 상태별 건수와 요약 정보를 계산합니다. + + +### DELETE /api/modules/sirsoft-board/admin/reports/{report} + +- **라우트명**: `api.modules.sirsoft-board.admin.reports.destroy` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\ReportController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.reports.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| report | path | string | 예 | — | 대상 report의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.manage`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 신고를 삭제합니다(소프트 삭제). `auth:sanctum` 인증과 `sirsoft-board.reports.manage` 권한이 필요합니다. 경로의 `report`(신고 ID)에 해당하는 신고 케이스를 소프트 삭제하며, 존재하지 않으면 404, 스코프 권한 위반 시 403을 반환합니다. + + +### GET /api/modules/sirsoft-board/admin/reports/{report} + +- **라우트명**: `api.modules.sirsoft-board.admin.reports.show` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\ReportController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.reports.view` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| report | path | string | 예 | — | 대상 report의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| board_id | integer | `4` | 게시판 ID (게시판 삭제 시 NULL) | +| board | object | `{"id":4,"name":"갤러리","slug":"gallery"}` | 신고 대상이 속한 게시판 정보 (id/name/slug). 게시판이 삭제된 경우 관계 대신 첫 신고 로그 스냅샷의 게시판명으로 폴백합니다. | +| target_type | string | `post` | 신고 대상 타입 (post, comment) | +| target_id | integer | `55` | 신고 대상 ID (동적 테이블의 ID) | +| post | object | `{"id":55,"title":"작업물 공유합니다","content":"최근에 작업한 결과물입니다.피드…` | 신고 대상 게시글 상세 (id/title/content/작성일시/작성자). 대상이 댓글이면 해당 댓글의 상위 게시글 정보가 담기며, 조회 불가 시 null 입니다. | +| comment | null | `null` | 신고 대상 댓글 상세 (id/content/작성일시/작성자). 대상 타입이 comment 일 때만 채워지고 게시글 신고에서는 null 입니다. | +| target_status | string | `published` | 신고 대상 콘텐츠의 현재 상태 (reportable의 current_status — 예: published, blinded). | +| target_status_label | string | `게시중` | `target_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| blind_trigger_type | string | `admin` | 대상 콘텐츠의 블라인드/상태 변경을 유발한 트리거 유형 (reportable의 trigger_type — 예: 자동/관리자 수동). | +| blind_trigger_type_label | string | `관리자 수동` | `blind_trigger_type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| status | string | `pending` | 신고 상태 (pending, review, rejected, suspended) | +| available_actions | array | `["review","rejected","suspended","deleted"]` | 현재 상태에서 전환 가능한 다음 신고 상태 목록 (상태 Enum의 getAvailableTransitions() 산물). 상태 변경 UI의 선택지로 사용됩니다. | +| abilities | object | `{"can_view":true,"can_manage":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | +| reporters | array | `[{"id":1,"reporter":{"uuid":"a1e0a91a-fba6-491c-a53e-7285…` | 이 케이스에 접수된 개별 신고 로그 목록. 각 항목은 신고자(reporter)·사유(reason_type/reason_detail)·신고 시점 스냅샷(snapshot)·신고 일시를 포함합니다. | +| report_count | integer | `1` | report 개수 (집계) | +| reason_summary | string | `욕설/비방 1건` | 신고 사유 요약 문자열. 상위 2개 사유를 "사유 N건" 형식으로 나열하고 나머지는 "외 N건"으로 합산해 표시합니다. | +| first_reported_at | string | `2026-06-04 09:35:42` | first reported 일시 | +| last_reported_at | string | `2026-05-13 09:35:42` | last reported 일시 | +| histories | array | `[{"id":1,"type":"reported","action_label":"신고 접수","proces…` | 신고 처리 이력 타임라인 (process_histories JSON 기반, 최신순). 각 항목은 이벤트 유형·라벨·처리자·사유·신고자 수·발생 일시를 포함하며, 접수 이벤트에는 처리자가 없습니다. | +| metadata | object | `{"ip":"127.0.0.1","user_agent":"Mozilla\/5.0 (Windows NT …` | 메타데이터 (IP, User Agent 등) | +| created_at | string | `2026-06-04 09:35:42` | 생성 일시 | +| updated_at | string | `2026-06-04 09:35:42` | 최종 수정 일시 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.view`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 신고 케이스 1건의 상세 정보를 조회합니다. `auth:sanctum` 인증과 `sirsoft-board.reports.view` 권한이 필요합니다. 경로의 `report`(신고 ID)를 기준으로 동일 대상에 대한 모든 신고를 그룹화한 상세 정보와 신고 대상 콘텐츠(reportable) 데이터를 함께 반환하며, 처리 이력(histories), 신고자 목록(reporters), 사유 요약(reason_summary), 전환 가능한 상태(available_actions) 등을 포함합니다. 대상이 없으면 404, 스코프 권한 위반 시 403을 반환합니다. + + +### GET /api/modules/sirsoft-board/admin/reports/{report}/reporters + +- **라우트명**: `api.modules.sirsoft-board.admin.reports.reporters` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\ReportController@reporters` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.reports.view` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| report | path | string | 예 | — | 대상 report의 식별자 | +| per_page | query | integer | 아니오 | min 1 | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| reporter | object | `{"uuid":"a1e0a91a-fba6-491c-a53e-7285a5686857","name":"관리…` | 개별 신고자 정보 (uuid/name/email). 비회원(게스트) 신고 로그인 경우 null 입니다. | +| reason_type | string | `abuse` | 신고 사유 코드 (사유 Enum 값 — 예: abuse, spam). | +| reason_type_label | string | `욕설/비방` | `reason_type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| reason_detail | string | `욕설과 비방이 포함된 게시글입니다. 다른 사용자를 모욕하는 내용이 …` | 신고자가 직접 입력한 상세 사유 텍스트. 입력하지 않은 경우 null 입니다. | +| snapshot | object | `{"board_name":"갤러리","title":"작업물 공유합니다","content":"최근에 작업…` | 신고 접수 시점의 대상 콘텐츠 스냅샷 (게시판명/제목/본문/작성자 등). 이후 대상이 수정·삭제되어도 신고 당시 내용을 보존합니다. | +| reported_at | string | `2026-06-04 09:35:42` | reported 일시 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.view`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 신고 케이스에 접수된 개별 신고자 목록을 페이지네이션으로 반환합니다. `auth:sanctum` 인증과 `sirsoft-board.reports.view` 권한이 필요합니다. 경로의 `report`(신고 케이스 ID)에 대해 각 신고자의 신고 사유(reason_type/reason_detail)와 신고 시점 스냅샷(snapshot)을 포함한 항목을 반환하며, `per_page`(최대 50)와 `page`로 페이지를 제어합니다. 대상이 없으면 404를 반환합니다. + + +### PATCH /api/modules/sirsoft-board/admin/reports/{report}/status + +- **라우트명**: `api.modules.sirsoft-board.admin.reports.update-status` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\ReportController@updateStatus` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.reports.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| report | path | string | 예 | — | 대상 report의 식별자 | +| status | body | string | 예 | — | 전환할 신고 상태 (`ReportStatus` 허용값). 현재 상태에서 전환 불가한 값(영구삭제 등)은 검증에서 422로 차단됩니다. | +| process_note | body | string | 아니오 | max 1000 | 처리 메모 (최대 1000자). 상태 변경 사유나 조치 내용을 처리 이력에 함께 기록합니다. | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.manage`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 단건 신고의 상태를 변경합니다. `auth:sanctum` 인증과 `sirsoft-board.reports.manage` 권한이 필요합니다. 경로의 `report`(신고 ID)에 대해 `status`로 지정한 상태로 전환하고 선택적으로 `process_note`(최대 1000자)를 처리 메모로 남깁니다. 영구삭제(deleted) 등 전환 불가 상태는 FormRequest 검증에서 422로 선차단되고 서비스의 deleted 가드가 2차 방어선으로 동작하며, 대상이 없으면 404를 반환합니다. + + diff --git a/modules/_bundled/sirsoft-board/docs/api/settings.md b/modules/_bundled/sirsoft-board/docs/api/settings.md new file mode 100644 index 00000000..83e98870 --- /dev/null +++ b/modules/_bundled/sirsoft-board/docs/api/settings.md @@ -0,0 +1,180 @@ +# Settings API 레퍼런스 + +> **소유**: module `sirsoft-board` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Settings 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-board/admin/settings + +- **라우트명**: `api.modules.sirsoft-board.admin.settings.index` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardSettingsController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`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":["admin","manager"],"manage_roles":["admin"]}` | 신고 관리 권한 역할. 신고 내역을 조회할 수 있는 역할(view_roles)과 신고를 처리·관리할 수 있는 역할(manage_roles)의 식별자 배열이며, 설정값이 아닌 DB 권한 데이터로 관리됩니다. | +| 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')` 기반 입력 제한값(페이지당 글 수·답글/댓글 깊이의 최소·최대 등)이 담겨 프론트 입력 검증 범위로 사용됩니다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.settings.read`)이 없는 경우 | + + + +**설명** 게시판 모듈의 전체 환경설정을 조회합니다. `auth:sanctum` 인증과 `sirsoft-board.settings.read` 권한이 필요합니다. 기본값(basic_defaults)/신고정책(report_policy)/스팸·보안(spam_security)/표시(display)/SEO/알림(notifications) 등 모든 카테고리 설정과 신고 권한 역할(report_permissions)을 함께 반환하며, 현재 사용자의 수정 가능 여부(abilities)와 입력 제한값(`_meta.limits`, `config('sirsoft-board.limits')`)을 포함합니다. + + +### PUT /api/modules/sirsoft-board/admin/settings + +- **라우트명**: `api.modules.sirsoft-board.admin.settings.store` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardSettingsController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| _tab | body | string | 아니오 | `basic_defaults`, `report_policy`, `spam_security`, `general`, `seo`, `notifications`, `notification_definitions` | 현재 편집 중인 탭을 나타내는 메타 값. 탭 단위 부분 저장의 컨텍스트를 식별하는 용도이며 설정값으로는 저장되지 않습니다. | +| notifications | body | array | 아니오 | — | 알림 채널 설정. `channels` 배열의 각 항목에 채널 식별자(id), 활성화 여부(is_active), 정렬 순서(sort_order)를 담아 저장합니다. | +| basic_defaults | body | array | 아니오 | — | 기본 설정 카테고리 값. 게시판 타입·페이지당 글 수·정렬·댓글/답글·길이 제한·파일 업로드·기본 권한 등 basic_defaults 하위 키를 저장합니다. | +| report_policy | body | array | 아니오 | — | 신고 정책 카테고리 값. 자동 숨김 임계치/대상, 일일 신고 한도, 거부 누적 제한, 관리자·작성자 신고 알림 설정을 저장합니다. | +| report_permissions | body | array | 아니오 | — | 신고 관리 권한 역할. `view_roles`(조회 역할)와 `manage_roles`(관리 역할)의 역할 식별자 배열이며, 포함 시 설정 저장과 별개로 DB 권한 역할이 동기화됩니다. | +| display | body | array | 아니오 | — | 표시 설정 카테고리 값. 날짜 표시 형식(date_display_format: standard/relative) 등을 저장합니다. | +| spam_security | body | array | 아니오 | — | 스팸·보안 카테고리 값. 글·댓글·신고 작성 쿨다운 시간(초)과 조회수 캐시 TTL을 저장합니다. | +| seo | body | array | 아니오 | — | SEO 설정 카테고리 값. 게시판 목록/개별/글 상세 페이지의 메타 제목·설명 템플릿과 각 페이지 SEO 활성화 여부를 저장합니다. | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 게시판 모듈의 환경설정을 저장합니다. `auth:sanctum` 인증과 `sirsoft-board.settings.update` 권한이 필요합니다. `_tab`으로 지정한 탭 단위로 검증된 설정을 저장하며, `report_permissions`가 포함된 경우 신고 권한 역할도 함께 동기화합니다. 저장 성공 시 갱신된 전체 설정과 신고 권한 역할을 반환합니다. + + +### POST /api/modules/sirsoft-board/admin/settings/bulk-apply + +- **라우트명**: `api.modules.sirsoft-board.admin.settings.bulk-apply` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardSettingsController@bulkApply` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| fields | body | array | 예 | min 1 | 대상 게시판에 일괄 적용할 필드 목록(최소 1개). boards 테이블 컬럼(type, per_page, use_comment, allowed_extensions 등)이나 권한 필드(default_board_permissions, manager), 점(.)을 포함한 개별 권한 키(예: `posts.read`)를 허용합니다. | +| apply_all | body | boolean | 예 | — | 전체 게시판 적용 여부. true면 모든 게시판에 적용하고, false면 `board_ids`로 지정한 게시판에만 적용합니다(false 시 board_ids 필수). | +| board_ids | body | array | 아니오 | — | board 식별자 배열 | +| override_values | body | array | 아니오 | — | 환경설정 기본값 대신 사용할 재정의 값 맵. 지정한 필드에 대해 기본값이 아닌 임의의 값으로 일괄 적용할 때 사용합니다. | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 환경설정의 기본값을 기존 게시판들에 일괄 적용합니다. `auth:sanctum` 인증과 `sirsoft-board.settings.update` 권한이 필요합니다. `fields`(적용할 필드, 최소 1개)와 `apply_all`(전체 적용 여부)을 받으며, `apply_all`이 false이면 `board_ids`로 대상을 지정하고 `override_values`로 값을 재정의할 수 있습니다. 적용 도중 실패하면 전체가 롤백되며, 이 경우에도 HTTP 200으로 `rolled_back: true`와 실패 지점 정보를 반환하여 프론트에서 안내 처리합니다. + + +### POST /api/modules/sirsoft-board/admin/settings/clear-cache + +- **라우트명**: `api.modules.sirsoft-board.admin.settings.clear-cache` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardSettingsController@clearCache` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.settings.update` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.settings.update`)이 없는 경우 | + + + +**설명** 게시판 모듈 설정 캐시를 초기화합니다. `auth:sanctum` 인증과 `sirsoft-board.settings.update` 권한이 필요합니다. ModuleSettings 캐시와 게시판 캐시를 모두 초기화하며, 응답으로 `cleared: true`를 반환합니다. + + +### GET /api/modules/sirsoft-board/admin/settings/{category} + +- **라우트명**: `api.modules.sirsoft-board.admin.settings.show` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardSettingsController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.settings.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| category | path | string | 예 | — | 분류 필터 (해당 분류의 항목만 조회) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-board.settings.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 카테고리의 설정만 조회합니다. `auth:sanctum` 인증과 `sirsoft-board.settings.read` 권한이 필요합니다. 경로의 `category`에 해당하는 설정만 반환하며, 응답에는 카테고리명(category), 설정값(settings), 현재 사용자의 수정 가능 여부(abilities)가 포함됩니다. + + diff --git a/modules/_bundled/sirsoft-board/docs/api/users.md b/modules/_bundled/sirsoft-board/docs/api/users.md new file mode 100644 index 00000000..951db707 --- /dev/null +++ b/modules/_bundled/sirsoft-board/docs/api/users.md @@ -0,0 +1,94 @@ +# Users API 레퍼런스 + +> **소유**: module `sirsoft-board` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Users 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-board/users/{user}/posts + +- **라우트명**: `api.modules.sirsoft-board.users.posts.index` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@userPosts` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| user | path | string | 예 | — | 대상 user의 식별자 | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `237` | 기본 키 (내부 식별자) | +| board_slug | string | `apidoc-sample-board` | 게시글이 속한 게시판의 슬러그(URL 식별자)입니다. 게시판 상세 링크 구성에 사용합니다. | +| board_name | string | `API 문서 샘플 게시판` | 게시글이 속한 게시판의 표시 이름입니다. 현재 로케일에 맞는 다국어 이름(`getLocalizedName()`)이 적용됩니다. | +| activity_type | string | `authored` | 활동 유형입니다. 공개 프로필의 게시글 목록은 작성글만 반환하므로 항상 `authored`(본인이 작성한 글)입니다. | +| activity_count | integer | `0` | activity 개수 (집계) | +| title | string | `API 문서 샘플 게시글` | 제목 | +| is_secret | boolean | `false` | secret 여부 | +| status | string | `published` | 계정 상태 (active: 활성, inactive: 비활성, blocked: 차단, withdrawn: 탈퇴) | +| view_count | integer | `43` | view 개수 (집계) | +| comment_count | integer | `0` | comment 개수 (집계) | +| created_at | string | `2026-07-07 09:34:50` | 생성 일시 | +| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| content_plain | string | `API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.` | 게시글 본문의 순수 텍스트입니다. HTML 모드 글은 태그를 제거한 평문으로 변환되며, 목록 미리보기용으로 사용합니다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 사용자(`{user}` 는 회원 uuid 로 라우트 바인딩)의 공개 프로필 페이지에 표시할 게시글 목록을 모든 게시판에 걸쳐 반환합니다. 비밀글은 제외되며, `optional.sanctum` 이 적용되어 비로그인 상태에서도 조회할 수 있습니다. `per_page`(1~100, 기본 20)와 `sort`(latest 등) 쿼리 파라미터로 페이지네이션·정렬을 제어합니다. + + +### GET /api/modules/sirsoft-board/users/{user}/posts/stats + +- **라우트명**: `api.modules.sirsoft-board.users.posts.stats` +- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@userPostsStats` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| user | path | string | 예 | — | 대상 user의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| posts_count | integer | `1` | posts 개수 (집계) | +| comments_count | integer | `1` | comments 개수 (집계) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 공개 프로필 페이지 상단에 표시할 특정 사용자(`{user}` 는 회원 uuid)의 게시글·댓글 수 요약을 반환합니다. `status=published` 인 항목만 집계하며, `optional.sanctum` 이 적용되어 비로그인 상태에서도 조회할 수 있습니다. + + diff --git a/modules/_bundled/sirsoft-board/src/Support/ApiDoc/ApiDocSampleService.php b/modules/_bundled/sirsoft-board/src/Support/ApiDoc/ApiDocSampleService.php new file mode 100644 index 00000000..9466e2fd --- /dev/null +++ b/modules/_bundled/sirsoft-board/src/Support/ApiDoc/ApiDocSampleService.php @@ -0,0 +1,290 @@ +}> 도메인 => 대표 레코드 정보 + */ + public function seed(): array + { + $board = $this->seedBoard(); + $post = $this->seedPost($board); + $comment = $this->seedComment($board, $post); + $attachment = $this->seedAttachment($board, $post); + + // 게시판 slug 라우팅(boards/{slug}/posts/{id}...)을 실측하기 위한 path 파라미터 맵. + // route-model binding 이 없는 문자열 param 을 실제 값으로 정확 일치 치환한다. + $boardParams = [ + 'slug' => (string) $board->slug, + 'board' => (string) $board->getKey(), + 'id' => (string) $post->getKey(), + 'postId' => (string) $post->getKey(), + 'commentId' => (string) $comment->getKey(), + 'hash' => (string) $attachment->hash, + ]; + + $map = []; + + // boards: 공개(User) 게시판/게시글/댓글/첨부 라우트 + $map['boards'] = [ + 'model' => Board::class, + 'key' => $board->getRouteKeyName(), + 'value' => (string) $board->getRouteKey(), + 'path_params' => $boardParams, + ]; + + // board: 관리자 게시글/첨부/댓글 라우트(admin/board/{slug}/posts/{id}...) + $map['board'] = [ + 'model' => Board::class, + 'key' => $board->getRouteKeyName(), + 'value' => (string) $board->getRouteKey(), + 'path_params' => $boardParams, + ]; + + // reports: 신고 상세(admin/reports/{report}) 실측용 대표 신고 + if ($report = $this->representativeReport($board, $post)) { + $map['reports'] = [ + 'model' => Report::class, + 'key' => $report->getRouteKeyName(), + 'value' => (string) $report->getRouteKey(), + 'path_params' => ['report' => (string) $report->getKey()], + ]; + } + + // board-types: 게시판 유형(GET 상세는 없으나 대표 키 노출) + if ($boardType = $this->representativeBoardType()) { + $map['board-types'] = [ + 'model' => BoardType::class, + 'key' => $boardType->getRouteKeyName(), + 'value' => (string) $boardType->getRouteKey(), + 'path_params' => ['id' => (string) $boardType->getKey()], + ]; + } + + return $map; + } + + /** + * 완전 샘플 게시판을 멱등 생성합니다(신고/파일 업로드 활성). + * + * @return Board 대표 게시판 레코드 + */ + private function seedBoard(): Board + { + $board = Board::query()->where('slug', self::SAMPLE_SLUG)->first() + ?? Board::factory()->create([ + 'slug' => self::SAMPLE_SLUG, + 'name' => ['ko' => 'API 문서 샘플 게시판', 'en' => 'API Doc Sample Board'], + 'is_active' => true, + 'use_report' => true, + 'use_file_upload' => true, + 'show_view_count' => true, + ]); + + // factory 직접 생성은 BoardService::create 의 권한 등록 훅을 우회하므로, + // 게시판별 권한(sirsoft-board.{slug}.{action})을 멱등 등록한다. 기본 role + // 매핑에 admin 이 포함(posts.read/comments.read 등)되어 실측 사용자(admin)가 + // slug 라우트(boards/{slug}/posts...)를 403 없이 실측할 수 있게 한다. + (new BoardPermissionService)->ensureBoardPermissions($board); + + return $board; + } + + /** + * 완전 샘플 공개 게시글을 멱등 생성합니다. + * + * @param Board $board 대표 게시판 + * @return Post 대표 게시글 레코드 + */ + private function seedPost(Board $board): Post + { + $post = Post::query() + ->where('board_id', $board->id) + ->where('title', 'API 문서 샘플 게시글') + ->first(); + + if ($post) { + return $post; + } + + $actor = $this->sampleActor(); + + return Post::query()->create([ + 'board_id' => $board->id, + 'title' => 'API 문서 샘플 게시글', + 'content' => '

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

', + 'content_mode' => 'html', + 'user_id' => $actor?->id, + 'author_name' => $actor?->name ?? '관리자', + 'ip_address' => '127.0.0.1', + 'is_notice' => false, + 'is_secret' => false, + 'status' => PostStatus::Published, + 'trigger_type' => TriggerType::User, + 'view_count' => 42, + ]); + } + + /** + * 완전 샘플 댓글을 멱등 생성합니다. + * + * @param Board $board 대표 게시판 + * @param Post $post 대표 게시글 + * @return Comment 대표 댓글 레코드 + */ + private function seedComment(Board $board, Post $post): Comment + { + $comment = Comment::query() + ->where('post_id', $post->id) + ->where('content', 'API 문서 샘플 댓글입니다.') + ->first(); + + if ($comment) { + return $comment; + } + + $actor = $this->sampleActor(); + + return Comment::query()->create([ + 'board_id' => $board->id, + 'post_id' => $post->id, + 'user_id' => $actor?->id, + 'author_name' => $actor?->name ?? '관리자', + 'content' => 'API 문서 샘플 댓글입니다.', + 'is_secret' => false, + 'status' => PostStatus::Published, + 'trigger_type' => TriggerType::User, + 'depth' => 0, + ]); + } + + /** + * 완전 샘플 첨부파일 레코드를 멱등 생성합니다(실파일 없이 메타만). + * + * @param Board $board 대표 게시판 + * @param Post $post 대표 게시글 + * @return Attachment 대표 첨부 레코드 + */ + private function seedAttachment(Board $board, Post $post): Attachment + { + $attachment = Attachment::query() + ->where('post_id', $post->id) + ->where('original_filename', 'apidoc-sample.png') + ->first(); + + if ($attachment) { + return $attachment; + } + + $actor = $this->sampleActor(); + + return Attachment::query()->create([ + 'board_id' => $board->id, + 'post_id' => $post->id, + 'hash' => 'apidocsmpl1', + 'original_filename' => 'apidoc-sample.png', + 'stored_filename' => 'apidoc-sample.png', + 'disk' => 'public', + 'path' => 'board/apidoc-sample.png', + 'mime_type' => 'image/png', + 'size' => 2048, + 'collection' => 'default', + 'order' => 0, + 'created_by' => $actor?->id, + 'trigger_type' => TriggerType::User, + ]); + } + + /** + * 신고 상세 실측용 대표 신고를 반환합니다(기존 우선, 없으면 멱등 생성). + * + * @param Board $board 대표 게시판 + * @param Post $post 대표 게시글 + * @return Report|null 대표 신고 (생성 실패 시 null) + */ + private function representativeReport(Board $board, Post $post): ?Report + { + if ($report = Report::query()->orderBy('id')->first()) { + return $report; + } + + $actor = $this->sampleActor(); + + return Report::query()->create([ + 'board_id' => $board->id, + 'target_type' => ReportType::Post, + 'target_id' => $post->id, + 'author_id' => $actor?->id, + 'status' => ReportStatus::Pending, + 'process_histories' => [], + 'metadata' => ['reason' => ReportReasonType::Spam->value], + 'last_reported_at' => now(), + ]); + } + + /** + * 대표 게시판 유형을 반환합니다(기존 우선, 없으면 멱등 생성). + * + * @return BoardType|null 대표 게시판 유형 (없으면 null) + */ + private function representativeBoardType(): ?BoardType + { + if ($boardType = BoardType::query()->orderBy('id')->first()) { + return $boardType; + } + + return BoardType::query()->create([ + 'slug' => 'apidoc-sample-type', + 'name' => ['ko' => 'API 문서 샘플 유형', 'en' => 'API Doc Sample Type'], + ]); + } + + /** + * 샘플 작성자로 쓸 사용자를 반환합니다. + * + * 코어 완전 샘플 사용자(먼저 시드됨)를 우선하고, 없으면 첫 사용자로 폴백합니다. + * + * @return User|null 샘플 사용자 (없으면 null) + */ + private function sampleActor(): ?User + { + return User::query()->where('email', 'apidoc-sample-user@example.com')->first() + ?? User::query()->orderBy('id')->first(); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Unit/Support/ApiDocSampleServiceTest.php b/modules/_bundled/sirsoft-board/tests/Unit/Support/ApiDocSampleServiceTest.php new file mode 100644 index 00000000..12aefbae --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Unit/Support/ApiDocSampleServiceTest.php @@ -0,0 +1,112 @@ +assertInstanceOf(ApiDocSampleSeeder::class, new ApiDocSampleService); + } + + #[Test] + public function 게시판_도메인_대표_샘플_맵을_반환한다(): void + { + $map = (new ApiDocSampleService)->seed(); + + // 공개(boards)/관리자(board) 두 도메인 키가 모두 존재 + $this->assertArrayHasKey('boards', $map); + $this->assertArrayHasKey('board', $map); + $this->assertSame(Board::class, $map['boards']['model']); + $this->assertNotEmpty($map['boards']['value']); + } + + #[Test] + public function slug_라우팅_실측용_path_params_맵을_제공한다(): void + { + $map = (new ApiDocSampleService)->seed(); + + // boards 도메인은 slug/board/id/postId/commentId/hash 를 실제 값으로 제공 + $params = $map['boards']['path_params']; + + $this->assertSame(self::SAMPLE_SLUG, $params['slug']); + $this->assertArrayHasKey('id', $params); + $this->assertArrayHasKey('postId', $params); + $this->assertArrayHasKey('commentId', $params); + $this->assertArrayHasKey('hash', $params); + + // 각 값은 실제 시드된 레코드의 키와 일치 + $post = Post::query()->where('title', 'API 문서 샘플 게시글')->firstOrFail(); + $comment = Comment::query()->where('content', 'API 문서 샘플 댓글입니다.')->firstOrFail(); + $attachment = Attachment::query()->where('original_filename', 'apidoc-sample.png')->firstOrFail(); + + $this->assertSame((string) $post->getKey(), $params['id']); + $this->assertSame((string) $post->getKey(), $params['postId']); + $this->assertSame((string) $comment->getKey(), $params['commentId']); + $this->assertSame((string) $attachment->hash, $params['hash']); + } + + #[Test] + public function 대표_샘플_게시판은_공개_게시글과_댓글과_첨부를_갖는다(): void + { + (new ApiDocSampleService)->seed(); + + $board = Board::query()->where('slug', self::SAMPLE_SLUG)->firstOrFail(); + $post = Post::query()->where('board_id', $board->id)->where('title', 'API 문서 샘플 게시글')->firstOrFail(); + + $this->assertTrue((bool) $board->is_active); + $this->assertFalse((bool) $post->is_secret); + $this->assertSame('published', $post->status->value); + $this->assertSame(1, Comment::query()->where('post_id', $post->id)->count()); + $this->assertSame(1, Attachment::query()->where('post_id', $post->id)->count()); + } + + #[Test] + public function 게시판별_권한이_admin_역할에_등록되어_실측_사용자가_접근할_수_있다(): void + { + (new ApiDocSampleService)->seed(); + + // ensureBoardPermissions 로 게시판별 권한이 생성됨 (posts.read 등) + $this->assertDatabaseHas('permissions', [ + 'identifier' => 'sirsoft-board.'.self::SAMPLE_SLUG.'.posts.read', + ]); + } + + #[Test] + public function 재실행_시_샘플이_중복_생성되지_않는다(): void + { + $service = new ApiDocSampleService; + + $service->seed(); + $service->seed(); + + $this->assertSame(1, Board::query()->where('slug', self::SAMPLE_SLUG)->count()); + $this->assertSame(1, Post::query()->where('title', 'API 문서 샘플 게시글')->count()); + $this->assertSame(1, Comment::query()->where('content', 'API 문서 샘플 댓글입니다.')->count()); + $this->assertSame(1, Attachment::query()->where('original_filename', 'apidoc-sample.png')->count()); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/addresses.md b/modules/_bundled/sirsoft-ecommerce/docs/api/addresses.md new file mode 100644 index 00000000..1a569a83 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/addresses.md @@ -0,0 +1,225 @@ +# Addresses API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Addresses 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/user/addresses + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.addresses.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserAddressController@index` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| addresses | object | `{"data":[{"id":263,"user_id":"a1e0a91a-fba6-491c-a53e-728…` | 회원 본인 소유 배송지 컬렉션 (`data[]` 배송지 항목 배열 + `abilities.can_create` — UserAddressCollection 파생) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 로그인한 회원 본인의 배송지 목록을 조회합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::getUserAddresses()`가 현재 사용자(`Auth::id()`) 소유의 배송지를 조회해 `UserAddressCollection`으로 반환합니다. 마이페이지 배송지 관리 화면이나 주문 시 배송지 선택 목록을 채우는 용도이며, 다른 회원의 배송지는 노출되지 않습니다. + + +### POST /api/modules/sirsoft-ecommerce/user/addresses + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.addresses.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserAddressController@store` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | string | 예 | max 100 | 대상의 이름/명칭 | +| recipient_name | body | string | 예 | max 50 | 수령인 이름 | +| recipient_phone | body | string | 예 | max 20 | 수령인 연락처 | +| country_code | body | string | 아니오 | — | 국가 코드 (ISO 3166-1 alpha-2) | +| zipcode | body | string | 아니오 | max 10 | 우편번호 | +| province_code | body | string | 아니오 | max 10 | 광역 시·도 코드 (국내 주소 지역 구분) | +| city | body | string | 아니오 | max 100 | 시·군·구 등 도시명 | +| address | body | string | 아니오 | max 255 | 기본 주소 | +| address_detail | body | string | 아니오 | max 255 | 상세 주소 | +| address_type_code | body | string | 아니오 | `R`, `J` | 국내 주소 표기 방식 (`R` 도로명 / `J` 지번) | +| address_line_1 | body | string | 아니오 | max 255 | 주소 1행 (기본 주소) | +| address_line_2 | body | string | 아니오 | max 255 | 주소 2행 (상세 주소) | +| intl_city | body | string | 아니오 | max 100 | 도시 (국제 주소) | +| intl_state | body | string | 아니오 | max 100 | 주/도 (국제 주소) | +| intl_postal_code | body | string | 아니오 | max 20 | 우편번호 (국제 주소) | +| is_default | body | boolean | 아니오 | — | 기본값 지정 여부 | +| force_overwrite | body | boolean | 아니오 | — | 동일 배송지명 존재 시 기존 항목 덮어쓰기 허용 (미지정 시 중복이면 409) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.user_address.store_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 로그인한 회원 본인의 새 배송지를 등록합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::createAddress()`가 검증된 요청에 현재 사용자 ID를 결합해 배송지를 생성하고 성공 시 `201`로 반환합니다. 국내(우편번호/도로명·지번)·해외(intl_* 필드) 주소를 모두 지원하고 `is_default`로 기본 배송지 지정이 가능합니다. 같은 이름의 배송지가 있으면 `409`(중복 ID 포함)를, `force_overwrite`로 덮어쓰기를 허용할 수 있으며, 최대 배송지 개수를 초과하면 `422`를 반환합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/user/addresses/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.addresses.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserAddressController@destroy` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 로그인한 회원 본인의 배송지 1건을 삭제합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::deleteAddress()`가 현재 사용자 소유 여부를 확인한 뒤 path의 `{id}` 배송지를 삭제합니다. 본인 소유가 아니거나 존재하지 않는 배송지이면 `404`를 반환합니다. 마이페이지 배송지 관리에서 더 이상 사용하지 않는 배송지를 제거하는 용도입니다. + + +### GET /api/modules/sirsoft-ecommerce/user/addresses/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.addresses.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserAddressController@show` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 로그인한 회원 본인의 배송지 1건 상세를 조회합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::getAddress()`가 현재 사용자 소유의 path `{id}` 배송지를 조회해 `UserAddressResource`로 반환합니다. 본인 소유가 아니거나 존재하지 않는 배송지이면 `404`를 반환합니다. 배송지 수정 화면 진입 시 기존 값을 불러오는 용도입니다. + + +### PUT /api/modules/sirsoft-ecommerce/user/addresses/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.addresses.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserAddressController@update` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| name | body | string | 아니오 | max 100 | 대상의 이름/명칭 | +| recipient_name | body | string | 아니오 | max 50 | 수령인 이름 | +| recipient_phone | body | string | 아니오 | max 20 | 수령인 연락처 | +| country_code | body | string | 아니오 | — | 국가 코드 (ISO 3166-1 alpha-2) | +| zipcode | body | string | 아니오 | max 10 | 우편번호 | +| province_code | body | string | 아니오 | max 10 | 광역 시·도 코드 (국내 주소 지역 구분) | +| city | body | string | 아니오 | max 100 | 시·군·구 등 도시명 | +| address | body | string | 아니오 | max 255 | 기본 주소 | +| address_detail | body | string | 아니오 | max 255 | 상세 주소 | +| address_type_code | body | string | 아니오 | `R`, `J` | 국내 주소 표기 방식 (`R` 도로명 / `J` 지번) | +| address_line_1 | body | string | 아니오 | max 255 | 주소 1행 (기본 주소) | +| address_line_2 | body | string | 아니오 | max 255 | 주소 2행 (상세 주소) | +| intl_city | body | string | 아니오 | max 100 | 도시 (국제 주소) | +| intl_state | body | string | 아니오 | max 100 | 주/도 (국제 주소) | +| intl_postal_code | body | string | 아니오 | max 20 | 우편번호 (국제 주소) | +| is_default | body | boolean | 아니오 | — | 기본값 지정 여부 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.user_address.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 로그인한 회원 본인의 배송지 1건을 수정합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::updateAddress()`가 현재 사용자 소유의 path `{id}` 배송지를 검증된 값으로 갱신하고 `UserAddressResource`로 반환합니다. 모든 본문 필드는 선택이며 전달된 필드만 갱신되고, 국내·해외 주소 필드와 `is_default`(기본 배송지 지정)를 모두 지원합니다. 본인 소유가 아니거나 존재하지 않는 배송지이면 `404`를 반환합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/user/addresses/{id}/default + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.addresses.set-default` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserAddressController@setDefault` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 로그인한 회원 본인의 배송지 1건을 기본 배송지로 지정합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::setDefaultAddress()`가 현재 사용자 소유의 path `{id}` 배송지를 기본으로 설정하고 기존 기본 배송지는 자동 해제됩니다. 본인 소유가 아니거나 존재하지 않는 배송지이면 `404`를 반환합니다. 마이페이지 배송지 목록에서 기본 배송지를 전환하는 용도입니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md b/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md new file mode 100644 index 00000000..1f62a7fc --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md @@ -0,0 +1,231 @@ +# Brands API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Brands 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/brands + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.brands.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\BrandController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.brands.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| is_active | query | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| search | query | string | 아니오 | max 100 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| sort | query | string | 아니오 | `name_asc`, `name_desc`, `created_asc`, `created_desc` | 정렬 기준 (필드명, `-` 접두 시 내림차순) | +| sort_by | query | string | 아니오 | `name`, `sort_order` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| locale | query | string | 아니오 | `ko`, `en`, `fr`, `ja` | 로케일 코드 (표시 언어/지역) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `43` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `127` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 브랜드","en":"API Doc Sample Brand"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `API 문서 샘플 브랜드` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| slug | string | `apidoc-sample-brand` | URL 친화 식별자 (slug) | +| url | string | `apidoc-sample-brand` | SortableMenuItem 표시용 URL (slug 값을 그대로 노출) | +| website | string | `https://www.asus.com` | 브랜드 공식 웹사이트 URL | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| is_active | boolean | `true` | active 여부 | +| icon | string | `tag` | 아이콘 식별자 (아이콘 클래스/이름) | +| created_at | string | `2026-07-07 14:47:31` | 생성 일시 | +| updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | +| creator | array | `[]` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| updater | array | `[]` | 수정자 정보 객체 (id/name — updater 관계 파생, 로드 시에만 포함) | +| products_count | integer | `0` | products 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자용 브랜드 목록을 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.brands.read` 권한이 필요하며, `BrandService::getAllBrands()`가 검증된 필터를 받아 조회합니다. `is_active`·`search`로 필터링하고 `sort`(name_asc/desc, created_asc/desc) 또는 `sort_by`+`sort_order` 조합으로 정렬하며, `locale`로 표시 언어를 지정할 수 있습니다. 각 항목에는 `products_count` 집계와 현재 사용자의 `abilities` 맵이 포함됩니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/brands + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.brands.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\BrandController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.brands.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| slug | body | string | 예 | max 200 | URL 친화 식별자 (slug) | +| website | body | string | 아니오 | max 500 | 브랜드 공식 웹사이트 URL | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.brand.create_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 새 브랜드를 생성합니다. `auth:sanctum` + `sirsoft-ecommerce.brands.create` 권한이 필요하며, `BrandService::createBrand()`가 트랜잭션 내에서 저장하고 생성자/수정자(`created_by`/`updated_by`)를 현재 사용자로 기록한 뒤 `BrandResource`를 201로 반환합니다. `name`(다국어 배열)과 `slug`가 필수이며 `website`·`sort_order`·`is_active`는 선택입니다. `sirsoft-ecommerce.brand.create_validation_rules` 필터로 확장이 검증 규칙을 추가할 수 있습니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/brands/{brand} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.brands.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\BrandController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.brands.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| brand | path | string | 예 | — | 대상 brand의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 브랜드 1건을 삭제합니다. `auth:sanctum` + `sirsoft-ecommerce.brands.delete` 권한이 필요하며, `BrandService::deleteBrand()`가 삭제 전 연결된 상품 수를 확인합니다. 연결된 상품이 1개 이상이면 예외가 발생해 삭제가 차단되고 400 오류가 반환되므로, 해당 브랜드의 상품을 먼저 정리하거나 다른 브랜드로 이전해야 삭제할 수 있습니다. 대상이 없으면 404를 반환합니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/brands/{brand} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.brands.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\BrandController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.brands.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| brand | path | string | 예 | — | 대상 brand의 식별자 | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| slug | body | string | 아니오 | — | URL 친화 식별자 (slug) | +| website | body | string | 아니오 | max 500 | 브랜드 공식 웹사이트 URL | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.brand.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 기존 브랜드(path의 `brand`)를 수정합니다. `auth:sanctum` + `sirsoft-ecommerce.brands.update` 권한이 필요하며, `BrandService::updateBrand()`가 검증된 데이터로 갱신한 뒤 `BrandResource`를 반환합니다. `name`은 필수이고 `slug`·`website`·`sort_order`·`is_active`는 선택입니다. 대상이 없거나 처리 중 예외가 발생하면 404 또는 400을 반환하며, `sirsoft-ecommerce.brand.update_validation_rules` 필터로 확장이 검증 규칙을 추가할 수 있습니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/brands/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.brands.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\BrandController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.brands.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 브랜드 1건의 상세 정보를 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.brands.read` 권한이 필요하며, path의 `id`로 `BrandService::getBrand()`를 호출해 단건을 `BrandResource`로 반환합니다. 응답에는 다국어 이름, 로컬라이즈된 이름(`localized_name`), `website`, `products_count` 집계, 현재 사용자의 `abilities` 맵이 포함됩니다. 대상 브랜드가 없으면 404를 반환합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/brands/{id}/toggle-status + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.brands.toggle-status` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\BrandController@toggleStatus` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.brands.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 브랜드의 활성 상태를 토글합니다. `auth:sanctum` + `sirsoft-ecommerce.brands.update` 권한이 필요하며, path의 `id`로 `BrandService::toggleStatus()`를 호출해 트랜잭션 내에서 현재 `is_active` 값을 반전시킨 뒤 갱신된 `BrandResource`를 반환합니다. 관리자 목록에서 브랜드 노출/비노출을 빠르게 전환할 때 사용하며, 대상이 없거나 처리 중 예외가 발생하면 404 또는 400을 반환합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/cart.md b/modules/_bundled/sirsoft-ecommerce/docs/api/cart.md new file mode 100644 index 00000000..a0df9a01 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/cart.md @@ -0,0 +1,338 @@ +# Cart API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Cart 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### DELETE /api/modules/sirsoft-ecommerce/cart + +- **라우트명**: `api.modules.sirsoft-ecommerce.cart.destroy-multiple` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CartController@destroyMultiple` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | query | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.cart.delete_items_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 장바구니에서 선택한 여러 아이템(`ids`)을 한 번에 삭제합니다. `optional.sanctum`으로 회원은 `Auth::id()`, 비회원은 cart_key(`X-Cart-Key`)로 소유를 식별하며, `CartController@destroyMultiple`이 `CartService::deleteItems()`를 호출해 삭제된 건수(`deleted_count`)를 반환합니다. 장바구니 화면에서 체크박스로 선택한 항목들을 "선택 삭제"할 때 사용합니다. + + +### GET /api/modules/sirsoft-ecommerce/cart + +- **라우트명**: `api.modules.sirsoft-ecommerce.cart.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CartController@index` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| selected_ids | query | array | 아니오 | — | selected 식별자 배열 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.cart.get_validation_rules`). + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| items | array | `[{"id":962,"product_id":201,"product_option_id":1086,"qua…` | 장바구니 라인 아이템 목록 (상품·옵션·수량 등 — CartItemResource 파생) | +| item_ids | array | `[962]` | item 식별자 배열 (연관 리소스 참조) | +| item_count | integer | `1` | item 개수 (집계) | +| calculation | object | `{"items":[{"product_id":201,"product_option_id":1086,"pro…` | 선택 아이템 기준 금액 계산 결과 (상품 소계·할인·배송비 등 — OrderCalculationResult 파생) | +| has_unshippable_items | boolean | `false` | unshippable items 여부 | +| selected_shipping_country | string | `KR` | 배송비 계산에 적용된 배송 국가 코드 (ResolveShippingCountry 해석 결과) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 장바구니 목록과 함께 가격 정보(소계·할인·배송비 등 `calculation`)를 계산해 반환합니다. `optional.sanctum`으로 회원은 `Auth::id()`, 비회원은 cart_key(`X-Cart-Key`)로 장바구니를 식별하며, `CartController@index`가 `CartService::getCartWithCalculation()`을 호출합니다. `selected_ids`를 전달하면 해당 아이템만 계산에 포함되고(미전달=전체, 빈 배열=계산 생략), 선택된 배송 국가로 배송 불가한 상품이 있으면 `has_unshippable_items`가 true가 됩니다. 비회원인데 cart_key가 없거나 형식(`ck_`+32자)이 틀리면 400을 반환합니다. + + +### POST /api/modules/sirsoft-ecommerce/cart + +- **라우트명**: `api.modules.sirsoft-ecommerce.cart.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CartController@store` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| product_id | body | integer | 예 | — | product 식별자 | +| items | body | array | 예 | min 1 | 처리 대상 항목 배열 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.cart.bulk_add_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 하나의 상품에 대해 단일 또는 여러 옵션 조합을 `items[]` 배열로 한 번에 장바구니에 담습니다. `optional.sanctum`으로 회원/비회원 모두 접근하며, `CartController@store`가 `CartService::bulkAddToCart()`를 호출한 뒤 추가된 아이템 목록과 총 담긴 수량(`cart_count`)을 201로 반환합니다. 재고 부족·판매 중지·구매 대상 제한·구매 수량 한도 위반은 사유별 422(cart_unavailable/purchase_not_allowed), 항목/권한/옵션 문제는 404/403/422로 매핑됩니다. 비회원은 cart_key(`X-Cart-Key`) 검증을 통과해야 합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/cart/all + +- **라우트명**: `api.modules.sirsoft-ecommerce.cart.destroy-all` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CartController@destroyAll` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 현재 회원/비회원 장바구니의 모든 아이템을 삭제해 장바구니를 비웁니다. `optional.sanctum`으로 회원은 `Auth::id()`, 비회원은 cart_key로 대상을 식별하며, `CartController@destroyAll`이 `CartService::deleteAll()`을 호출해 삭제된 건수(`deleted_count`)를 반환합니다. 장바구니 화면의 "전체 비우기" 동작에 사용합니다. + + +### GET /api/modules/sirsoft-ecommerce/cart/count + +- **라우트명**: `api.modules.sirsoft-ecommerce.cart.count` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CartController@count` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| selected_ids | query | array | 아니오 | — | selected 식별자 배열 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.cart.get_validation_rules`). + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| count | integer | `1` | 장바구니에 담긴 아이템 개수 (`selected_ids` 지정 시 해당 항목만 집계) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 장바구니에 담긴 아이템 개수(`count`)만 가볍게 조회합니다. `optional.sanctum`으로 회원/비회원 모두 접근하며, `CartController@count`가 `CartService::getItemCount()`를 호출합니다. 전체 목록·계산 결과가 필요 없는 헤더의 장바구니 배지 카운트 갱신 등에 사용하며, `selected_ids`로 특정 아이템만 집계할 수도 있습니다. + + +### POST /api/modules/sirsoft-ecommerce/cart/key + +- **라우트명**: `api.modules.sirsoft-ecommerce.cart.key` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CartController@issueCartKey` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 비회원 장바구니를 식별하기 위한 cart_key(`ck_`+32자 영숫자)를 발급합니다. 인증이 필요 없으며(`optional.sanctum`), `CartController@issueCartKey`가 `CartService::issueCartKey()`로 키를 생성해 반환합니다. 비회원은 이 키를 `X-Cart-Key` 헤더에 실어 이후 장바구니 담기·조회·수정 요청에서 자신의 장바구니를 식별합니다. 비회원 쇼핑 시작 시점(첫 장바구니 담기 전)에 한 번 호출해 클라이언트에 저장해 둡니다. + + +### POST /api/modules/sirsoft-ecommerce/cart/merge + +- **라우트명**: `api.modules.sirsoft-ecommerce.cart.merge` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CartController@merge` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 비회원 상태에서 담아 둔 장바구니를 로그인한 회원 계정으로 병합합니다. 로그인 직후 클라이언트가 보유한 cart_key(`X-Cart-Key`)를 실어 호출하면, `CartController@merge`가 `CartService::mergeGuestCartToUser()`로 해당 cart_key의 비회원 아이템을 회원 장바구니로 옮기고 병합된 건수(`merged_count`)를 반환합니다. 비회원으로 담던 상품이 로그인 후 사라지지 않도록 인증 성공 시점에 1회 호출합니다. + + +### POST /api/modules/sirsoft-ecommerce/cart/query + +- **라우트명**: `api.modules.sirsoft-ecommerce.cart.query` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CartController@index` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| selected_ids | body | array | 아니오 | — | selected 식별자 배열 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.cart.get_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** GET `/cart`와 동일하게 장바구니 목록과 가격 계산 결과를 반환하되, `selected_ids`를 GET 쿼리 대신 POST 본문으로 전달하는 변형 엔드포인트입니다(같은 `CartController@index` 처리). 선택 아이템 배열이 커서 URL 길이 제한이 우려되는 경우에 사용합니다. `optional.sanctum`으로 회원/비회원 모두 접근하며, 응답 구조(`items`·`calculation`·`has_unshippable_items` 등)는 GET 조회와 동일합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/cart/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.cart.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CartController@destroy` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 장바구니에서 단일 아이템(`id`)을 삭제합니다. `optional.sanctum`으로 회원은 `Auth::id()`, 비회원은 cart_key(`X-Cart-Key`)로 소유를 식별하며, `CartController@destroy`가 `CartService::deleteItem()`을 호출합니다. 존재하지 않는 항목은 404, 타인 소유 항목 삭제 시도는 403(사유별 `CartOperationException` 매핑)으로 반환합니다. 장바구니 각 행의 개별 삭제 버튼에 사용합니다. + + +### PUT /api/modules/sirsoft-ecommerce/cart/{id}/option + +- **라우트명**: `api.modules.sirsoft-ecommerce.cart.change-option` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CartController@changeOption` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| product_option_id | body | integer | 예 | — | product option 식별자 | +| quantity | body | integer | 예 | min 1, max 9999 | 변경할 구매 수량 (1~9999) | +| additional_option_selections | body | array | 아니오 | — | 추가 옵션 재선택 목록 (항목별 additional_option_id/value_id, 직접입력 custom_text — 미전달 시 기존 선택 유지) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.cart.change_option_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 장바구니 아이템(`id`)의 선택 옵션과 수량을 변경합니다. `optional.sanctum`으로 회원/비회원 모두 접근하며, `CartController@changeOption`이 `CartService::changeOption()`으로 `product_option_id`·`quantity`(및 추가 옵션 선택)를 반영한 뒤 수정된 아이템을 반환합니다. 다른 상품의 옵션으로 바꾸려 하거나 옵션이 없는 경우, 재고/판매상태/구매수량 한도 위반은 사유별 422/404/403으로 매핑됩니다. 장바구니에서 옵션(예: 색상/사이즈)을 바꿀 때 사용합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/cart/{id}/quantity + +- **라우트명**: `api.modules.sirsoft-ecommerce.cart.update-quantity` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CartController@updateQuantity` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| quantity | body | integer | 예 | min 1, max 9999 | 변경할 구매 수량 (1~9999) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.cart.update_quantity_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 장바구니 아이템(`id`)의 수량만 변경합니다. `optional.sanctum`으로 회원/비회원 모두 접근하며, `CartController@updateQuantity`가 `CartService::updateQuantity()`로 수량을 반영한 뒤 프론트가 별도 refetch 없이 화면을 갱신할 수 있도록 `index`와 동일한 전체 목록·계산 결과(`items`·`calculation`)를 함께 반환합니다. 수량은 1~9999 범위이며, 재고 부족·판매 중지·구매수량 한도 위반은 사유별 422, 항목/권한 문제는 404/403으로 매핑됩니다. 장바구니의 수량 증감(+/-) 컨트롤에 사용합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/categories.md b/modules/_bundled/sirsoft-ecommerce/docs/api/categories.md new file mode 100644 index 00000000..bbc75ea7 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/categories.md @@ -0,0 +1,513 @@ +# Categories API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Categories 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/categories + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.categories.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CategoryController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.categories.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| parent_id | query | string | 아니오 | — | parent 식별자 | +| is_active | query | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| search | query | string | 아니오 | max 100 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| hierarchical | query | boolean | 아니오 | — | true 면 자식을 중첩한 트리 구조로 반환 | +| flat | query | boolean | 아니오 | — | true 면 깊이 들여쓰기를 포함한 평면 리스트로 반환 (TagInput 등에 사용) | +| max_depth | query | integer | 아니오 | min 1, max 10 | 조회할 최대 계층 깊이 제한 (1~10) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `87` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"의류","en":"Clothing"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| description | object | `{"ko":"다양한 스타일의 의류 제품","en":"Various styles of clothing p…` | 설명 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `의류` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| path | string | `87` | 조상부터 자기 자신까지의 ID를 `/`로 이은 materialized path (조상 조회·하위 일괄 선택에 사용) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| is_active | boolean | `true` | active 여부 | +| slug | string | `clothing` | URL 친화 식별자 (slug) | +| url | string | `clothing` | SortableMenuItem 표시용 URL (slug 값을 그대로 사용) | +| icon | string | `folder` | 아이콘 식별자 (아이콘 클래스/이름) | +| meta_title | null | `null` | SEO 메타 제목 (검색엔진/소셜 공유 표시 제목, 미설정 시 null) | +| meta_description | null | `null` | SEO 메타 설명 (검색엔진/소셜 공유 표시 요약, 미설정 시 null) | +| created_at | string | `2026-06-15 02:24:00` | 생성 일시 | +| updated_at | string | `2026-06-15 02:24:00` | 최종 수정 일시 | +| images | array | `[]` | 카테고리 이미지 배열 (images 관계 로드 시 — id/hash/download_url/alt_text 등) | +| products_count | integer | `22` | products 개수 (집계) | +| children_count | integer | `0` | children 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자용 카테고리 목록을 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.read` 권한이 필요하며, `CategoryService::getHierarchicalCategories()`가 검증된 필터(`parent_id`/`is_active`/`search`/`max_depth`)를 받아 조회합니다. `hierarchical=true`면 자식을 중첩한 트리, `flat=true`면 평면 리스트(TagInput 등에 사용), 둘 다 없으면 기본 계층 구조를 반환합니다. 각 항목에는 `products_count`·`children_count` 집계와 현재 사용자의 `abilities` 맵이 포함됩니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/categories + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.categories.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CategoryController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.categories.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| description | body | array | 아니오 | — | 설명 | +| parent_id | body | string | 아니오 | — | parent 식별자 | +| slug | body | string | 예 | max 200 | URL 친화 식별자 (slug) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| meta_title | body | string | 아니오 | max 200 | SEO 메타 제목 (검색엔진/소셜 공유 표시 제목) | +| meta_description | body | string | 아니오 | — | SEO 메타 설명 (검색엔진/소셜 공유 표시 요약) | +| temp_key | body | string | 아니오 | max 64 | 저장 전 임시 업로드한 이미지를 이 카테고리에 연결하기 위한 FileUploader temp_key | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category.create_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 새 카테고리를 생성합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.create` 권한이 필요하며, `CategoryService::createCategory()`가 검증된 데이터로 저장한 뒤 `CategoryResource`를 201로 반환합니다. `name`(다국어 배열)과 `slug`는 필수이고, `parent_id`를 지정하면 해당 카테고리의 하위로 배치되어 path/depth가 계산됩니다. `temp_key`로 사전 업로드해 둔 임시 이미지를 이 시점에 카테고리에 연결할 수 있으며, `sirsoft-ecommerce.category.create_validation_rules` 필터로 확장이 추가 파라미터를 검증에 주입할 수 있습니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/categories/images + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.categories.images.upload-temp` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CategoryController@uploadImage` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.categories.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| file | body | file | 예 | max 10240 | 업로드 파일 | +| temp_key | body | string | 아니오 | max 64 | 사전 업로드한 임시 이미지를 이 카테고리에 연결하기 위한 FileUploader temp_key | +| collection | body | string | 아니오 | max 100 | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) | +| alt_text | body | array | 아니오 | — | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category-image.filter_upload_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 카테고리에 아직 귀속되지 않은 이미지를 임시로 업로드합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.update` 권한이 필요하며, path에 `categoryId`가 없으므로 `CategoryImageService::upload()`가 `temp_key` 기준의 임시 이미지로 저장합니다. 카테고리 생성/수정 폼에서 저장 전에 이미지를 먼저 올릴 때 사용하며, 이후 store/update 요청에 같은 `temp_key`를 전달하면 해당 카테고리에 연결됩니다. 응답은 FileUploader 컴포넌트가 기대하는 `data.data` 형식으로 업로드 이미지의 id/hash/download_url 등을 201로 반환합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/categories/images/reorder + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.categories.images.reorder` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CategoryController@reorderImages` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.categories.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | body | array | 예 | min 1 | 이미지 순서 배열 (각 항목 `{id, order}` — 이미지 id별 새 정렬 순서) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category-image.filter_reorder_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 카테고리 이미지들의 표시 순서를 일괄 변경합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.update` 권한이 필요하며, `order` 배열(각 항목 `{id, order}`)을 받아 컨트롤러가 `id => order` 맵으로 변환한 뒤 `CategoryImageService::reorder()`에 전달합니다. 여러 이미지를 등록한 카테고리에서 드래그로 순서를 재배열할 때 사용하며, `sirsoft-ecommerce.category-image.filter_reorder_validation_rules` 필터로 확장이 검증 규칙을 추가할 수 있습니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/categories/images/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.categories.images.delete` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CategoryController@deleteImage` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.categories.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 카테고리 이미지 1건을 삭제합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.update` 권한이 필요하며, path의 이미지 `id`로 `CategoryImageService::delete()`를 호출해 레코드와 저장 파일을 제거합니다. 대상 이미지가 존재하지 않으면 404를 반환합니다. 카테고리 편집 화면에서 등록된 이미지나 임시 업로드 이미지를 개별 제거할 때 사용합니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/categories/order + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.categories.reorder` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CategoryController@reorder` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.categories.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| parent_menus | body | array | 아니오 | — | 최상위 카테고리 순서 배열 (SortableMenuList — 각 항목 `{id, order}`, child_menus 없으면 필수) | +| child_menus | body | array | 아니오 | — | 부모 ID별 자식 카테고리 순서 맵 (`{부모id: [{id, order}, ...]}`, parent_menus 없으면 필수) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category.reorder_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 카테고리 트리 전체의 배치(부모-자식 관계와 정렬 순서)를 일괄 갱신합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.update` 권한이 필요하며, SortableMenuList 컴포넌트가 보내는 `parent_menus`/`child_menus` 형식을 컨트롤러가 `{id, parent_id, sort_order}` 목록으로 변환해 `CategoryService::reorder()`(트랜잭션)에 전달합니다. `parent_id`가 바뀐 항목은 depth와 materialized path가 함께 재계산됩니다. 관리자 카테고리 관리 화면에서 드래그 앤 드롭으로 계층 구조를 재정렬할 때 사용합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/categories/tree + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.categories.tree` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CategoryController@tree` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.categories.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `87` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"의류","en":"Clothing"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| description | object | `{"ko":"다양한 스타일의 의류 제품","en":"Various styles of clothing p…` | 설명 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `의류` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| path | string | `87` | 조상부터 자기 자신까지의 ID를 `/`로 이은 materialized path (조상 조회·하위 일괄 선택에 사용) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| is_active | boolean | `true` | active 여부 | +| slug | string | `clothing` | URL 친화 식별자 (slug) | +| url | string | `clothing` | SortableMenuItem 표시용 URL (slug 값을 그대로 사용) | +| icon | string | `folder` | 아이콘 식별자 (아이콘 클래스/이름) | +| meta_title | null | `null` | SEO 메타 제목 (검색엔진/소셜 공유 표시 제목, 미설정 시 null) | +| meta_description | null | `null` | SEO 메타 설명 (검색엔진/소셜 공유 표시 요약, 미설정 시 null) | +| created_at | string | `2026-06-15 02:24:00` | 생성 일시 | +| updated_at | string | `2026-06-15 02:24:00` | 최종 수정 일시 | +| parent | null | `null` | 상위 항목 객체 (parent 관계 파생) | +| children | array | `[{"id":88,"name":{"ko":"남성","en":"Men"},"description":{"k…` | 하위 항목 배열 (계층 트리 — children 관계 파생) | +| images | array | `[]` | 카테고리 이미지 배열 (images 관계 로드 시 — id/hash/download_url/alt_text 등) | +| products_count | integer | `22` | products 개수 (집계) | +| children_count | integer | `2` | children 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.read`)이 없는 경우 | + + + +**설명** 상품 등록 폼용 카테고리 트리를 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.read` 권한이 필요하며, 별도 파라미터 없이 `CategoryService::getHierarchicalCategories(['hierarchical' => true, 'is_active' => true])`를 호출해 활성 카테고리만 자식을 중첩한 트리로 반환합니다. index 엔드포인트와 달리 필터를 받지 않고 항상 활성 트리를 반환하므로, 상품 작성/수정 시 카테고리 선택 UI를 채우는 용도로 사용됩니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/categories/{categoryId}/images + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.categories.images.upload` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CategoryController@uploadImage` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.categories.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| categoryId | path | string | 예 | — | 대상 category의 식별자 | +| file | body | file | 예 | max 10240 | 업로드 파일 | +| temp_key | body | string | 아니오 | max 64 | 사전 업로드한 임시 이미지를 이 카테고리에 연결하기 위한 FileUploader temp_key | +| collection | body | string | 아니오 | max 100 | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) | +| alt_text | body | array | 아니오 | — | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category-image.filter_upload_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 카테고리(path의 `categoryId`)에 이미지 1건을 업로드해 즉시 귀속시킵니다. `auth:sanctum` + `sirsoft-ecommerce.categories.update` 권한이 필요하며, `CategoryImageService::upload()`가 `categoryId`와 함께 파일을 저장하므로 임시 업로드와 달리 해당 카테고리에 바로 연결됩니다. 대상 카테고리가 없으면 404를 반환하고, 응답은 FileUploader가 기대하는 `data.data` 형식으로 업로드 이미지 정보를 201로 반환합니다. 이미 존재하는 카테고리를 편집하며 이미지를 추가할 때 사용합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/categories/{category} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.categories.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CategoryController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.categories.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| category | path | string | 예 | — | 분류 필터 (해당 분류의 항목만 조회) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 카테고리 1건을 삭제합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.delete` 권한이 필요하며, `CategoryService::deleteCategory()`가 삭제 전 안전 검사를 수행합니다. 하위 카테고리가 있거나 연결된 상품이 존재하면 예외가 발생해 삭제가 차단되고 400 오류가 반환되므로, 자식과 상품을 먼저 정리해야 삭제할 수 있습니다. 대상이 없으면 404를 반환합니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/categories/{category} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.categories.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CategoryController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.categories.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| category | path | string | 예 | — | 분류 필터 (해당 분류의 항목만 조회) | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| description | body | array | 아니오 | — | 설명 | +| parent_id | body | string | 아니오 | — | parent 식별자 | +| slug | body | string | 예 | max 200 | URL 친화 식별자 (slug) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| meta_title | body | string | 아니오 | max 200 | SEO 메타 제목 (검색엔진/소셜 공유 표시 제목) | +| meta_description | body | string | 아니오 | — | SEO 메타 설명 (검색엔진/소셜 공유 표시 요약) | +| temp_key | body | string | 아니오 | max 64 | 저장 전 임시 업로드한 이미지를 이 카테고리에 연결하기 위한 FileUploader temp_key | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 기존 카테고리(path의 `category`)를 수정합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.update` 권한이 필요하며, `CategoryService::updateCategory()`가 검증된 데이터로 갱신한 뒤 `CategoryResource`를 반환합니다. `name`과 `slug`는 필수이고, `parent_id`를 변경하면 계층 위치(path/depth)가 재계산됩니다. `temp_key`로 임시 업로드한 이미지를 이 시점에 연결할 수 있으며, 대상이 없거나 처리 중 예외가 발생하면 404 또는 400을 반환합니다. `sirsoft-ecommerce.category.update_validation_rules` 필터로 확장이 검증 규칙을 추가할 수 있습니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/categories/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.categories.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CategoryController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.categories.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 카테고리 1건의 상세 정보를 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.read` 권한이 필요하며, path의 `id`로 `CategoryService::getCategory()`를 호출해 단건을 `CategoryResource`로 반환합니다. 응답에는 부모(`parent`)·자식(`children`)·이미지(`images`) 관계와 `products_count`·`children_count` 집계, 현재 사용자의 `abilities` 맵이 포함됩니다. 대상 카테고리가 없으면 404를 반환합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/categories/{id}/toggle-status + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.categories.toggle-status` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CategoryController@toggleStatus` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.categories.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 카테고리의 활성 상태를 토글합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.update` 권한이 필요하며, path의 `id`로 `CategoryService::toggleStatus()`를 호출해 현재 `is_active` 값을 반전시킨 뒤 갱신된 `CategoryResource`를 반환합니다. 관리자 목록에서 노출/비노출을 빠르게 전환할 때 사용하며, 대상이 없거나 처리 중 예외가 발생하면 404 또는 400을 반환합니다. + + +### GET /api/modules/sirsoft-ecommerce/categories + +- **라우트명**: `api.modules.sirsoft-ecommerce.categories.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CategoryController@index` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `87` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"의류","en":"Clothing"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| name_localized | string | `의류` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| slug | string | `clothing` | URL 친화 식별자 (slug) | +| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | +| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | +| products_count | integer | `22` | products 개수 (집계) | +| children | array | `[{"id":88,"name":{"ko":"남성","en":"Men"},"name_localized":…` | 하위 항목 배열 (계층 트리 — children 관계 파생) | + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 공개 카테고리 트리를 조회합니다. 인증이 필요 없는 공개 엔드포인트이며, `Public\CategoryController@index`가 `CategoryService::getPublicCategoryTree()`를 호출해 활성 카테고리만 자식을 중첩한 트리로 반환합니다. 각 항목에는 로컬라이즈된 이름(`name_localized`)과 공개 상품 수(`products_count`)가 포함됩니다. 스토어프론트의 카테고리 내비게이션/메뉴를 렌더링하는 데 사용하며, 조회 실패 시 500을 반환합니다. + + +### GET /api/modules/sirsoft-ecommerce/categories/{slug} + +- **라우트명**: `api.modules.sirsoft-ecommerce.categories.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CategoryController@show` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** slug로 단일 공개 카테고리와 직계 자식을 조회합니다. 인증이 필요 없는 공개 엔드포인트이며, `Public\CategoryController@show`가 `CategoryService::getPublicCategoryBySlug()`를 호출해 활성 자식(`activeChildren`)과 이미지를 함께 로드합니다. 조회된 카테고리가 비활성(`is_active=false`)이면 없는 것으로 간주해 404를 반환하며, 응답에는 상위 경로를 나타내는 `breadcrumb` 배열과 `products_count` 집계가 포함됩니다. 스토어프론트 카테고리 상세/목록 페이지 진입 시 사용합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/category-image.md b/modules/_bundled/sirsoft-ecommerce/docs/api/category-image.md new file mode 100644 index 00000000..3217d16b --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/category-image.md @@ -0,0 +1,46 @@ +# Category Image API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Category Image 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/category-image/{hash} + +- **라우트명**: `api.modules.sirsoft-ecommerce.category-image.download` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CategoryImageController@download` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 공개 API로, 해시(`hash`)로 식별되는 카테고리 이미지 원본 파일을 스트리밍 서빙합니다. 인증이 필요 없으며(`PublicBaseController`), `CategoryImageController@download`가 `CategoryImageService::download()`를 호출해 리포지토리에서 해시로 이미지를 찾고 `StorageInterface::response()`로 `StreamedResponse`를 반환합니다. 응답에는 저장된 `mime_type`과 `Cache-Control: public, max-age=31536000`(1년) 헤더가 부여되어 브라우저/CDN 캐싱에 최적화됩니다. 해시에 해당하는 레코드가 없거나 스토리지에 실제 파일이 없으면 404를, 그 외 처리 오류 시 400 에러 응답을 반환합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/checkout.md b/modules/_bundled/sirsoft-ecommerce/docs/api/checkout.md new file mode 100644 index 00000000..881f88cd --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/checkout.md @@ -0,0 +1,163 @@ +# Checkout API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Checkout 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### DELETE /api/modules/sirsoft-ecommerce/checkout + +- **라우트명**: `api.modules.sirsoft-ecommerce.checkout.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CheckoutController@destroy` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 주문서 페이지를 이탈할 때 현재 회원/비회원의 임시 주문(temp order)을 삭제합니다. `optional.sanctum`으로 회원은 `Auth::id()`, 비회원은 cart_key(`X-Cart-Key`)로 대상을 식별하며, `CheckoutController@destroy`가 `TempOrderService::deleteTempOrder()`를 호출합니다. 삭제할 임시 주문이 없으면 404를 반환합니다. 주문 확정 없이 주문서에서 뒤로가기·페이지 이탈 시 미완료 임시 데이터를 정리하는 용도입니다. + + +### GET /api/modules/sirsoft-ecommerce/checkout + +- **라우트명**: `api.modules.sirsoft-ecommerce.checkout.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CheckoutController@show` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| country_code | query | string | 아니오 | — | 국가 코드 (ISO 3166-1 alpha-2) | +| zipcode | query | string | 아니오 | max 20 | 우편번호 | +| region | query | string | 아니오 | max 100 | 지역/권역 | +| city | query | string | 아니오 | max 100 | 도시명 (배송비 미리보기 산출용 배송 주소) | +| address | query | string | 아니오 | max 255 | 기본 주소 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 현재 유효한 임시 주문을 조회하면서 최신 가격으로 실시간 재계산해 주문서 페이지 데이터를 반환합니다. `optional.sanctum`으로 회원/비회원 모두 접근하며, `CheckoutController@show`가 `TempOrderService::getTempOrderWithCalculation()`으로 재계산하고 `CheckoutDataService::buildResponseData()`가 쿠폰·마일리지·상품·구매불가 상품 정보를 포함해 응답을 구성합니다. 쿼리로 `country_code`/`zipcode`/`region` 등 배송 주소를 전달하면 해당 주소 기준 배송비가 계산되며, 우편번호 없이 배송국가만으로도 미리보기 배송비를 산출합니다. 임시 주문이 만료·미존재면 404를 반환합니다. + + +### POST /api/modules/sirsoft-ecommerce/checkout + +- **라우트명**: `api.modules.sirsoft-ecommerce.checkout.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CheckoutController@store` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| item_ids | body | array | 아니오 | min 1 | item 식별자 배열 | +| direct_items | body | array | 아니오 | min 1 | 바로 구매 항목 배열 (장바구니 미경유 — 항목별 product_id/option_values/quantity, item_ids와 택일) | +| coupon_issue_ids | body | array | 아니오 | — | coupon issue 식별자 배열 | +| use_points | body | integer | 아니오 | min 0 | 사용할 마일리지(적립금) 포인트 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.checkout.validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 장바구니에서 선택한 아이템으로 임시 주문을 생성해 주문서 작성 단계로 진입합니다. `optional.sanctum`으로 회원/비회원 모두 접근하며, `CheckoutController@store`가 `direct_items`가 있으면 `TempOrderService::createTempOrderFromDirectItems()`(바로 구매, 장바구니 미경유), 없으면 `item_ids`로 `createTempOrderFromSelectedItems()`(장바구니 경유)를 호출합니다. 응답에는 임시 주문 ID·계산 결과·만료 시각(`expires_at`)이 포함됩니다. 재고 부족·판매 중지·구매 제한 상품이 있으면 400(cart_unavailable), 보유 잔액을 넘는 마일리지 사용은 422, 빈 장바구니는 400을 반환합니다. + + +### PUT /api/modules/sirsoft-ecommerce/checkout + +- **라우트명**: `api.modules.sirsoft-ecommerce.checkout.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CheckoutController@update` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| item_coupons | body | array | 아니오 | — | 상품별 적용 쿠폰 맵 (상품 옵션 ID를 키로, 발급 쿠폰 ID 배열을 값으로 — 상품당 최대 2개) | +| order_coupon_issue_id | body | integer | 아니오 | — | order coupon issue 식별자 | +| shipping_coupon_issue_id | body | integer | 아니오 | — | shipping coupon issue 식별자 | +| use_points | body | integer | 아니오 | min 0 | 사용할 마일리지(적립금) 포인트 | +| zipcode | body | string | 아니오 | max 10 | 우편번호 | +| country_code | body | string | 아니오 | — | 국가 코드 (ISO 3166-1 alpha-2) | +| payment_method | body | string | 아니오 | max 50 | 결제 수단 코드 (결제수단별 할인/수수료 계산 확장용) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.checkout.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 주문서 작성 중 쿠폰·마일리지·배송 주소가 변경될 때 임시 주문 금액을 재계산합니다. `optional.sanctum`으로 회원/비회원 모두 접근하며, `CheckoutController@update`가 전송된 프로모션 필드(`item_coupons`·`order_coupon_issue_id`·`shipping_coupon_issue_id`)와 `use_points`만 반영하고 미전송 필드는 `TempOrderService::updateTempOrder()`에서 기존 값을 유지합니다. `zipcode`/`country_code`로 배송 주소를 함께 넘기면 배송비가 다시 계산됩니다. 임시 주문이 만료·미존재면 404, 보유 잔액 초과 마일리지는 422를 반환합니다. + + +### POST /api/modules/sirsoft-ecommerce/checkout/extend + +- **라우트명**: `api.modules.sirsoft-ecommerce.checkout.extend` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\CheckoutController@extend` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 임시 주문의 만료 시각을 연장합니다. `optional.sanctum`으로 회원은 `Auth::id()`, 비회원은 cart_key로 대상을 식별하며, `CheckoutController@extend`가 `TempOrderService::extendExpiration()`을 호출해 갱신된 `expires_at`을 반환합니다. 주문서 작성이 길어져 임시 주문이 만료되기 전에 세션을 연장하는 용도이며, 연장할 임시 주문이 이미 만료·미존재면 404를 반환합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/claim-reasons.md b/modules/_bundled/sirsoft-ecommerce/docs/api/claim-reasons.md new file mode 100644 index 00000000..00616675 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/claim-reasons.md @@ -0,0 +1,312 @@ +# Claim Reasons API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Claim Reasons 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/claim-reasons + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.claim-reasons.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ClaimReasonController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `8` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `1` | 기본 키 (내부 식별자) | +| type | string | `refund` | 클래임 사유 유형 (ClaimReasonTypeEnum — `refund`(환불/취소)) | +| code | string | `order_mistake` | 사유 식별 코드 (같은 type 내 고유, 영문 소문자/숫자/`_`) | +| name | object | `{"ko":"주문 실수","en":"Order Mistake","ja":"注文ミス"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `주문 실수` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| fault_type | string | `customer` | 귀책 구분 (ClaimReasonFaultTypeEnum — `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-05-27 15:20:43` | 생성 일시 | +| updated_at | string | `2026-06-27 00:49:51` | 최종 수정 일시 | +| creator | array | `[]` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| updater | array | `[]` | 최종 수정자 정보 객체 (id/name — updater 관계 로드 시) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.read`)이 없는 경우 | + + + +**설명** 관리자가 클래임(반품/교환/환불) 사유 마스터 목록을 조회합니다. `permission:sirsoft-ecommerce.settings.read` 권한이 필요하며, `type`(기본 refund)·`is_active`·`fault_type`·`search` 쿼리로 필터링할 수 있습니다. `ClaimReasonService::getAllReasons()`가 조회하고 `ClaimReasonCollection`으로 반환하며, 각 항목은 다국어 사유명(`name`)과 귀책 구분(`fault_type`: customer·seller·carrier), 사용자 노출 여부(`is_user_selectable`)를 포함합니다. 환경설정의 클래임 사유 관리 화면에서 사용됩니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/claim-reasons + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.claim-reasons.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ClaimReasonController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| type | body | string | 예 | — | 클래임 사유 유형 (ClaimReasonTypeEnum — 현재 `refund`(환불/취소)) | +| code | body | string | 예 | max 50 | 사유 식별 코드 (영문 소문자/숫자/`_`, 같은 type 내에서 고유) | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| fault_type | body | string | 예 | — | 귀책 구분 (ClaimReasonFaultTypeEnum — `customer`(고객)/`seller`(판매자)/`carrier`(배송사)) | +| is_user_selectable | body | boolean | 아니오 | — | user selectable 여부 | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 새 클래임 사유를 생성합니다. `permission:sirsoft-ecommerce.settings.update` 권한이 필요하며, 유형(`type`)·고유 코드(`code`)·다국어 사유명(`name`)·귀책 구분(`fault_type`)을 필수로 받고 사용자 노출 여부·활성 여부·정렬 순서를 선택 입력합니다. `ClaimReasonService::createReason()`이 저장하고 생성된 사유를 201로 반환합니다. `code` 는 유형 내에서 고유해야 하며 회원 취소/반품 화면의 사유 선택지로 활용됩니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/claim-reasons/active + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.claim-reasons.active` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ClaimReasonController@active` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `8` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `1` | 기본 키 (내부 식별자) | +| type | string | `refund` | 클래임 사유 유형 (ClaimReasonTypeEnum — `refund`(환불/취소)) | +| code | string | `order_mistake` | 사유 식별 코드 (같은 type 내 고유, 영문 소문자/숫자/`_`) | +| name | object | `{"ko":"주문 실수","en":"Order Mistake","ja":"注文ミス"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `주문 실수` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| fault_type | string | `customer` | 귀책 구분 (ClaimReasonFaultTypeEnum — `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-05-27 15:20:43` | 생성 일시 | +| updated_at | string | `2026-06-27 00:49:51` | 최종 수정 일시 | +| creator | array | `[]` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| updater | array | `[]` | 최종 수정자 정보 객체 (id/name — updater 관계 로드 시) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.read`)이 없는 경우 | + + + +**설명** 활성화된 클래임 사유만 추려 Select 옵션용으로 조회합니다. `permission:sirsoft-ecommerce.settings.read` 권한이 필요하며, `type` 쿼리(기본 refund)로 유형을 지정하면 `ClaimReasonService::getActiveReasons()`가 `is_active=true` 인 사유만 반환합니다. 관리자 화면에서 환불/취소 처리 시 사유 드롭다운을 채우는 용도로, 목록(index)과 달리 필터 없이 활성 사유만 내려주는 점이 다릅니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/claim-reasons/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.claim-reasons.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ClaimReasonController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 클래임 사유 1건을 삭제합니다. `permission:sirsoft-ecommerce.settings.update` 권한이 필요하며, 대상이 없으면 404 를 반환하고 존재하면 `ClaimReasonService::deleteReason()`이 삭제합니다. 이미 사용 중인 사유 등 삭제 불가 상황에서는 서비스가 던진 예외 메시지를 그대로 사용해 400 으로 응답하므로, 관리자에게 삭제 실패 사유가 노출됩니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/claim-reasons/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.claim-reasons.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ClaimReasonController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 클래임 사유 1건의 상세 정보를 조회합니다. `permission:sirsoft-ecommerce.settings.read` 권한이 필요하며, `ClaimReasonService::getReason()`이 대상을 조회해 `ClaimReasonResource`로 반환합니다. 사유 편집 폼을 열 때 기존 값(다국어 사유명·코드·귀책 구분·활성/노출 설정 등)을 채우는 용도로 사용되며, 해당 사유가 없으면 404 를 반환합니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/claim-reasons/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.claim-reasons.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ClaimReasonController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| type | body | string | 예 | — | 클래임 사유 유형 (ClaimReasonTypeEnum — 현재 `refund`(환불/취소)) | +| code | body | string | 예 | max 50 | 사유 식별 코드 (영문 소문자/숫자/`_`, 같은 type 내에서 고유) | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| fault_type | body | string | 예 | — | 귀책 구분 (ClaimReasonFaultTypeEnum — `customer`(고객)/`seller`(판매자)/`carrier`(배송사)) | +| is_user_selectable | body | boolean | 아니오 | — | user selectable 여부 | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 기존 클래임 사유를 수정합니다. `permission:sirsoft-ecommerce.settings.update` 권한이 필요하고 대상은 경로 `id` 로 지정하며, 생성과 동일한 필드(유형·코드·다국어 사유명·귀책 구분·노출/활성/정렬)를 받아 `ClaimReasonService::updateReason()`이 갱신하고 갱신된 사유를 반환합니다. 대상이 없거나 갱신 실패 시 400 오류로 응답합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/claim-reasons/{id}/toggle-status + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.claim-reasons.toggle-status` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ClaimReasonController@toggleStatus` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 클래임 사유의 활성 상태를 켜고 끄는 토글을 수행합니다. `permission:sirsoft-ecommerce.settings.update` 권한이 필요하며, `ClaimReasonService::toggleStatus()`가 대상 사유의 `is_active` 값을 반전시켜 저장하고 갱신된 사유를 반환합니다. 사유를 삭제하지 않고 일시적으로 회원 선택지에서 감추거나 다시 노출할 때 사용합니다. + + +### GET /api/modules/sirsoft-ecommerce/user/claim-reasons + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.claim-reasons.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ClaimReasonController@userSelectableReasons` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-ecommerce.user-orders.cancel` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `7` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `1` | 기본 키 (내부 식별자) | +| type | string | `refund` | 클래임 사유 유형 (ClaimReasonTypeEnum — `refund`(환불/취소)) | +| code | string | `order_mistake` | 사유 식별 코드 (같은 type 내 고유, 영문 소문자/숫자/`_`) | +| name | object | `{"ko":"주문 실수","en":"Order Mistake","ja":"注文ミス"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `주문 실수` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| fault_type | string | `customer` | 귀책 구분 (ClaimReasonFaultTypeEnum — `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-05-27 15:20:43` | 생성 일시 | +| updated_at | string | `2026-06-27 00:49:51` | 최종 수정 일시 | +| creator | array | `[]` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| updater | array | `[]` | 최종 수정자 정보 객체 (id/name — updater 관계 로드 시) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-orders.cancel`)이 없는 경우 | + + + +**설명** 회원(및 선택적으로 비회원)이 주문 취소/반품 신청 화면에서 선택할 수 있는 클래임 사유 목록을 조회합니다. `optional.sanctum`(회원/비회원 모두 접근)과 `permission:sirsoft-ecommerce.user-orders.cancel` 권한이 적용되며, `ClaimReasonService::getUserSelectableReasons()`가 활성이면서 `is_user_selectable=true` 인 사유만 반환합니다. 관리 전용 목록과 달리 사용자에게 공개 가능한 사유만 내려주는 사용자향 엔드포인트입니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/coupons.md b/modules/_bundled/sirsoft-ecommerce/docs/api/coupons.md new file mode 100644 index 00000000..32d268bc --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/coupons.md @@ -0,0 +1,190 @@ +# Coupons API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Coupons 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/user/coupons + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.coupons.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserCouponController@index` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| status | query | string | 아니오 | `available`, `used`, `expired` | 상태 필터 (해당 상태의 항목만 조회) | +| per_page | query | integer | 아니오 | min 1, max 50 | 페이지당 항목 수 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.coupon.user_list_validation_rules`). + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| coupons | object | `{"data":[{"id":8611,"coupon_id":156,"user_id":"a1e0a91a-f…` | 회원이 발급받은 쿠폰(발급 내역) 페이지네이션 객체 (`data[]` 발급 건 + `pagination` — CouponIssueCollection 직렬화, 쿠폰 정의가 아닌 회원별 발급 건) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 로그인한 회원이 마이페이지 쿠폰함에서 자신이 발급받은 쿠폰(발급 내역)을 페이지네이션으로 조회합니다. `auth:sanctum` 인증이 필요하며, `status` 필터(available·used·expired)로 사용 가능/사용 완료/만료 쿠폰을 구분합니다. `UserCouponService::getUserCoupons()`가 조회하고 `CouponIssueCollection`으로 직렬화되므로, 여기의 항목은 쿠폰 정의(마스터)가 아니라 회원별 발급 건입니다. + + +### GET /api/modules/sirsoft-ecommerce/user/coupons/available + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.coupons.available` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserCouponController@available` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| product_ids | query | array | 아니오 | — | product 식별자 배열 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.coupon.user_available_validation_rules`). + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| coupons | array | `[{"id":4702,"coupon_id":106,"user_id":1,"coupon_code":nul…` | 현재 장바구니 상품에 적용 가능한 보유 쿠폰(발급 건) 배열 (상품/카테고리 범위·최소 주문금액·유효기간을 만족해 주문에 곧바로 선택 가능한 후보만) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 체크아웃 화면에서 회원이 현재 장바구니 상품에 실제로 적용할 수 있는 보유 쿠폰만 추려서 반환합니다. `auth:sanctum` 인증이 필요하고 `product_ids` 로 대상 상품을 전달하면, `UserCouponService::getAvailableCoupons()`가 보유 쿠폰 중 상품/카테고리 적용 범위·최소 주문금액·유효기간 등을 만족하는 것만 필터링해 내려줍니다. 쿠폰함 목록(index)과 달리 주문에 곧바로 선택 가능한 후보만 반환하는 점이 다릅니다. + + +### GET /api/modules/sirsoft-ecommerce/user/coupons/downloadable + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.coupons.downloadable` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserCouponController@downloadable` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| per_page | query | integer | 아니오 | min 1, max 50 | 페이지당 항목 수 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.coupon.user_downloadable_validation_rules`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `157` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 쿠폰","en":"API Doc Sample Coupon"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| description | object | `{"ko":"특정 카테고리 배송비 할인","en":"Shipping discount on categor…` | 설명 (다국어 필드는 로케일별 값 객체) | +| target_type | string | `order_amount` | 할인 적용 대상 (product_amount=상품금액, order_amount=주문금액, shipping_fee=배송비) | +| discount_type | string | `fixed` | 혜택 유형 (fixed=정액 할인, rate=정률(%) 할인) | +| discount_value | string | `1000.00` | 혜택값 (정액이면 할인 금액, 정률이면 할인율 %) | +| discount_max_amount | string | `3000.00` | 정률 할인 시 최대 할인 금액 상한 (없으면 null) | +| min_order_amount | string | `0.00` | 쿠폰 적용 최소 주문금액 (미만 주문에는 사용 불가) | +| issue_method | string | `download` | 발급 방법 (direct=직접발급, download=다운로드, auto=자동발급) | +| issue_condition | string | `manual` | 발급 조건 (manual=수동, signup=회원가입, first_purchase=첫구매, birthday=생일) | +| issue_status | string | `issuing` | 발급 상태 (issuing=발급중, stopped=발급중단) | +| total_quantity | integer | `300` | 총 발급 수량 상한 (null=무제한) | +| issued_count | integer | `0` | issued 개수 (집계) | +| per_user_limit | integer | `1` | 회원 1인당 발급 제한 수량 | +| valid_type | string | `period` | 유효기간 유형 (period=기간지정, days_from_issue=발급일로부터 N일) | +| valid_days | integer | `14` | 발급일로부터 유효한 일수 (valid_type=days_from_issue 인 경우) | +| valid_from | string | `2026-06-08T02:24:18.000000Z` | 유효기간 시작일 (쿠폰 사용 가능 시작 시각) | +| valid_to | string | `2026-08-07T02:24:18.000000Z` | 유효기간 종료일 (쿠폰 사용 가능 종료 시각) | +| issue_from | string | `2026-06-08T02:24:18.000000Z` | 발급기간 시작일 (다운로드 가능 시작 시각) | +| issue_to | string | `2026-07-15T02:24:18.000000Z` | 발급기간 종료일 (다운로드 가능 종료 시각) | +| is_combinable | boolean | `false` | combinable 여부 | +| target_scope | string | `all` | 적용 범위 (all=전체 상품, products=특정 상품, categories=특정 카테고리) | +| created_by | integer | `1` | 쿠폰 등록자(관리자) 식별자 (users 참조, 삭제 시 null) | +| created_at | string | `2026-07-07T05:47:31.000000Z` | 생성 일시 | +| updated_at | string | `2026-07-07T05:47:31.000000Z` | 최종 수정 일시 | +| deleted_at | null | `null` | 소프트 삭제 일시 (미삭제 시 null) | +| is_downloaded | boolean | `false` | downloaded 여부 | +| user_issued_count | integer | `0` | user issued 개수 (집계) | +| coupon_id | integer | `157` | coupon 식별자 (연관 리소스 참조) | +| localized_name | string | `API 문서 샘플 쿠폰` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| target_type_short_label | string | `주문` | `target_type_short` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| valid_period_formatted | string | `-` | `valid_period` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| min_order_amount_formatted | string | `0원` | `min_order_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| remaining_quantity | integer | `300` | 잔여 발급 가능 수량 (total_quantity − issued_count, 무제한이면 null) | +| benefit_formatted | string | `1,000원 할인` | `benefit` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| included_products | array | `[]` | 적용 대상 포함 상품 목록 (target_scope=products 시 이 상품에만 적용) | +| excluded_products | array | `[]` | 적용 제외 상품 목록 (해당 상품은 쿠폰 적용에서 제외) | +| included_categories | array | `[]` | 적용 대상 포함 카테고리 목록 (target_scope=categories 시 이 카테고리에만 적용) | +| excluded_categories | array | `[]` | 적용 제외 카테고리 목록 (해당 카테고리는 쿠폰 적용에서 제외) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 로그인한 회원이 지금 다운로드해 발급받을 수 있는 쿠폰(다운로드형) 목록을 페이지네이션으로 조회합니다. `auth:sanctum` 인증이 필요하며, `UserCouponService::getDownloadableCoupons()`가 발급기간·수량·회원당 한도를 만족하는 다운로드형 쿠폰을 반환하고 각 항목에 `is_downloaded`·`user_issued_count`로 이미 받았는지 여부를 표시합니다. 여기 항목은 아직 발급 전이므로 쿠폰 정의(마스터) 기준이며, 실제 발급은 download 엔드포인트로 수행합니다. + + +### POST /api/modules/sirsoft-ecommerce/user/coupons/{couponId}/download + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.coupons.download` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserCouponController@download` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| couponId | path | string | 예 | — | 대상 coupon의 식별자 | +| coupon_id | body | integer | 예 | — | coupon 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 로그인한 회원이 다운로드형 쿠폰 1건을 실제로 발급받습니다. `auth:sanctum` 인증이 필요하고 대상 쿠폰은 `couponId`(경로)로 지정하며, `UserCouponService::downloadCoupon()`이 발급기간·수량·회원당 한도·중복 발급 여부를 검증한 뒤 발급 내역을 생성해 201로 반환합니다. 한도 초과·발급기간 종료 등 발급 불가 사유는 서비스 예외의 메시지와 코드가 그대로 오류 응답에 전달됩니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/currency.md b/modules/_bundled/sirsoft-ecommerce/docs/api/currency.md new file mode 100644 index 00000000..24784ecc --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/currency.md @@ -0,0 +1,76 @@ +# Currency API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Currency 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/user/currency + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.currency.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserCurrencyController@show` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| preferred_currency | null | `null` | 회원이 저장한 선호 결제 통화 코드 (미설정 시 `null`) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 로그인한 회원 본인의 선호 결제 통화를 조회합니다. `auth:sanctum` 인증이 필요하며, `UserCurrencyService::getPreferredCurrency()`가 현재 사용자의 저장된 통화 코드를 반환합니다. 아직 통화를 설정하지 않은 회원은 `preferred_currency`가 `null`로 반환됩니다. 마이페이지 통화 설정 화면이나 회원정보 수정 화면에서 현재 값을 표시하는 용도입니다. + + +### PUT /api/modules/sirsoft-ecommerce/user/currency + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.currency.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserCurrencyController@update` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| currency | body | string | 예 | — | 저장할 선호 결제 통화 코드 (등록 통화: is_default 또는 exchange_rate>0 인 통화만 허용) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 로그인한 회원 본인의 선호 결제 통화를 저장합니다. `auth:sanctum` 인증이 필요하며, `UserCurrencyService::setPreferredCurrency()`가 검증된 `currency`(등록된 통화만 허용)를 현재 사용자에 영속화하고 저장된 통화 코드를 반환합니다. 마이페이지 통화 설정이나 회원정보 수정에서 회원이 직접 결제 통화를 변경할 때 호출하는 용도입니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/dashboard.md b/modules/_bundled/sirsoft-ecommerce/docs/api/dashboard.md new file mode 100644 index 00000000..11190efe --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/dashboard.md @@ -0,0 +1,162 @@ +# Dashboard API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Dashboard 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/dashboard/overview + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.dashboard.overview` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\DashboardController@overview` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.dashboard.view` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| pending_payment | integer | `0` | 오늘 결제대기 상태 주문상품 수량 | +| payment_complete | integer | `0` | 오늘 결제완료 상태 주문상품 수량 | +| preparing | integer | `0` | 오늘 상품준비중 상태 주문상품 수량 | +| shipping_ready | integer | `0` | 오늘 배송준비 상태 주문상품 수량 | +| shipping | integer | `0` | 오늘 배송중 상태 주문상품 수량 | +| cancellations | integer | `0` | 오늘 취소 상태 주문상품 수량 (전체취소 기준, 부분취소 포함) | +| returns | integer | `0` | 오늘 반품 상태 주문상품 수량 (환불 도메인 미반영으로 현재 항상 0) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.dashboard.view`)이 없는 경우 | + + + +**설명** 관리자 대시보드 상단의 "오늘 주문 현황" 배지를 채우는 조회 엔드포인트입니다. `auth:sanctum` + admin + `sirsoft-ecommerce.dashboard.view` 권한이 필요하며, `EcommerceDashboardService::getOverview()`가 오늘자 주문을 상태별(결제대기/결제완료/상품준비중/배송준비/배송중/취소/반품)로 집계해 각 건수를 반환합니다. 모든 값은 오늘 하루 범위의 집계이며, 관리자가 처리해야 할 주문 흐름을 한눈에 파악하는 용도입니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/dashboard/pending-inquiries + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.dashboard.pending-inquiries` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\DashboardController@pendingInquiries` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.dashboard.view` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| limit | query | integer | 아니오 | min 1, max 50 | 반환할 최대 항목 수 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| items | array | `[{"id":1,"product_id":320,"inquirable_id":79762,"product_…` | 미답변 상품문의 목록 (최신순, PendingInquiryResource — 문의 id/상품/작성자/게시판 글 id 등) | +| total | integer | `1` | 전체 개수 (집계) | +| board_slug | null | `null` | 문의가 저장된 연동 게시판의 slug (관리자 문의 상세 링크용, 미연동 시 null) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.dashboard.view`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 전체 상품에 달린 미답변 상품문의 목록과 총 건수를 조회합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.dashboard.view` 권한이 필요하며, `EcommerceDashboardService::getPendingInquiries()`가 답변 대기 문의를 최신순으로 조회해 `items`(문의 목록)·`total`(전체 미답변 건수)·`board_slug`(연동 게시판 slug, 미연동 시 null)를 반환합니다. `limit` 쿼리로 표시 건수를 1~50 사이에서 조정할 수 있고, 미지정 시 모듈 설정 `dashboard.recent_limit`(기본 5) 값이 적용됩니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/dashboard/recent-reviews + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.dashboard.recent-reviews` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\DashboardController@recentReviews` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.dashboard.view` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| limit | query | integer | 아니오 | min 1, max 50 | 반환할 최대 항목 수 | + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `99` | 기본 키 (내부 식별자) | +| product_id | integer | `320` | product 식별자 (연관 리소스 참조) | +| product_name | string | `API 문서 샘플 상품` | 리뷰 대상 상품의 현재 로케일 상품명 (product 관계 파생) | +| rating | integer | `5` | 리뷰 평점 (별점 정수) | +| author_name | string | `API 문서 샘플 사용자` | 리뷰 작성자 이름 (user 관계 파생) | +| created_at | string | `2026-07-07 14:47:31` | 생성 일시 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.dashboard.view`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 전체 상품에 등록된 최신 노출 리뷰를 조회해 대시보드 "최신 리뷰" 카드를 채웁니다. `auth:sanctum` + admin + `sirsoft-ecommerce.dashboard.view` 권한이 필요하며, `EcommerceDashboardService::getRecentReviews()`가 노출 상태의 리뷰를 최신순으로 가져와 `RecentReviewResource`로 상품명·평점·작성자명·작성일시 등을 반환합니다. `limit` 쿼리로 1~50 건 범위에서 표시 개수를 지정할 수 있으며, 미지정 시 모듈 설정 `dashboard.recent_limit`(기본 5)이 적용됩니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/dashboard/sales-graph + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.dashboard.sales-graph` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\DashboardController@salesGraph` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.dashboard.view` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| days | array | `[{"date":"2026-07-01","sales_quantity":0,"sales_amount":0…` | 일자별 판매 집계 배열 (각 항목 `{date, sales_quantity, sales_amount}` — 그래프 막대 데이터) | +| total_quantity | integer | `0` | 표시 기간 판매 수량 합계 | +| total_sales | integer | `0` | 표시 기간 순매출 합계 (기본 통화 자릿수로 라운딩) | +| quantity_change | null | `null` | 직전 동일 기간 대비 판매 수량 증감율(%) (직전 합계 0 이면 null) | +| sales_change | null | `null` | 직전 동일 기간 대비 순매출 증감율(%) (직전 합계 0 이면 null) | +| updated_at | null | `null` | 최종 수정 일시 | +| updated_at_display | string | `` | 집계 마지막 갱신 시각의 사용자 타임존 HH:mm 캡션 (갱신 이력 없으면 빈 문자열) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.dashboard.view`)이 없는 경우 | + + + +**설명** 최근 N일간의 판매 추세 막대 그래프 데이터를 조회합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.dashboard.view` 권한이 필요하며, `EcommerceDashboardService::getSalesGraph()`가 일자별 판매 수량·금액(`days`)과 기간 합계(`total_quantity`, `total_sales`), 직전 기간 대비 변화율(`quantity_change`, `sales_change`)을 반환합니다. 그래프 표시 일수는 모듈 설정 `dashboard.graph_days`(기본 7일)로 결정되며 별도 파라미터는 받지 않습니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/extra-fee-templates.md b/modules/_bundled/sirsoft-ecommerce/docs/api/extra-fee-templates.md new file mode 100644 index 00000000..a299a5c7 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/extra-fee-templates.md @@ -0,0 +1,343 @@ +# Extra Fee Templates API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Extra Fee Templates 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/extra-fee-templates + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.extra-fee-templates.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ExtraFeeTemplateController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| search | query | string | 아니오 | max 200 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| region | query | string | 아니오 | max 100 | 지역/권역 | +| is_active | query | string | 아니오 | ``, `true`, `false` | 활성 여부 (true 활성 / false 비활성) | +| sort_by | query | string | 아니오 | `id`, `zipcode`, `fee`, `region`, `is_active`, `created_at`, `updated_at` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| per_page | query | integer | 아니오 | min 10, max 100 | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `37` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `109` | 기본 키 (내부 식별자) | +| zipcode | string | `00000` | 추가배송비를 적용할 우편번호 (단일 또는 `-` 로 이은 범위) | +| fee | integer | `3000` | 해당 우편번호에 부과할 추가 배송비 (상점 기본 통화 기준 반올림) | +| fee_formatted | string | `3,000원` | `fee` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| region | string | `경기 안산 풍도동` | 지역명 (도서산간 등 관리자 참고용 표시 라벨) | +| description | string | `도서산간 지역` | 설명 (다국어 필드는 로케일별 값 객체) | +| is_active | boolean | `true` | active 여부 | +| created_by | null | `null` | 등록자 UUID (creator 관계 파생, 없으면 null) | +| updated_by | null | `null` | 최종 수정자 UUID (updater 관계 파생, 없으면 null) | +| created_at | string | `2026-07-07 14:47:31` | 생성 일시 | +| updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 추가배송비(도서산간 등 우편번호별 할증) 템플릿 목록을 검색·필터·정렬·페이지네이션으로 조회합니다. `permission:sirsoft-ecommerce.shipping-policies.read` 권한이 필요하며, `search`·`region`·`is_active` 필터와 다양한 정렬 기준을 지원합니다. `ExtraFeeTemplateService::getList()`가 조회하고 통계(`getStatistics()`)를 함께 담아 `ExtraFeeTemplateCollection`으로 반환합니다. 각 항목은 우편번호·추가 배송비·지역명을 포함하며 배송정책의 지역별 할증 관리 화면에 사용됩니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/extra-fee-templates + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.extra-fee-templates.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ExtraFeeTemplateController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| zipcode | body | string | 예 | max 20 | 우편번호 | +| fee | body | number | 예 | min 0, max 9999999999.99 | 해당 우편번호에 부과할 추가 배송비 (0 이상) | +| region | body | string | 아니오 | max 100 | 지역/권역 | +| description | body | string | 아니오 | max 1000 | 설명 | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 추가배송비 템플릿 1건을 생성합니다. `permission:sirsoft-ecommerce.shipping-policies.create` 권한이 필요하며, 우편번호(`zipcode`, 단일 또는 범위)와 추가 배송비(`fee`)를 필수로 받고 지역명·설명·활성 여부를 선택 입력합니다. `ExtraFeeTemplateService::create()`가 저장하고 생성된 템플릿을 201로 반환합니다. 활성 템플릿은 배송비 계산 시 해당 우편번호에 할증으로 반영됩니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/active-settings + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.extra-fee-templates.active-settings` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ExtraFeeTemplateController@activeSettings` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| zipcode | string | `00000` | 추가배송비 적용 우편번호 (배송정책 설정용 축약 필드) | +| fee | integer | `3000` | 해당 우편번호의 추가 배송비 (float 변환값) | +| region | string | `` | 지역명 (없으면 빈 문자열) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.read`)이 없는 경우 | + + + +**설명** 활성화된 추가배송비 템플릿을 배송정책에서 바로 사용할 수 있는 축약 JSON 배열(우편번호·배송비·지역명)로 반환합니다. `permission:sirsoft-ecommerce.shipping-policies.read` 권한이 필요하며, `ExtraFeeTemplateService::getAllAsExtraFeeSettings()`가 `is_active=true` 인 템플릿만 배송정책 설정 형식으로 변환합니다. 배송정책 편집 화면에서 지역별 할증 규칙을 채우거나 계산 로직에 주입하는 용도로, 관리 목록(index)의 상세 필드 대신 계산에 필요한 최소 필드만 내려줍니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/bulk + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.extra-fee-templates.bulk-destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ExtraFeeTemplateController@bulkDestroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | query | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.delete`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 여러 추가배송비 템플릿을 한 번에 삭제합니다. `permission:sirsoft-ecommerce.shipping-policies.delete` 권한이 필요하고 대상 ID 배열(`ids`)을 쿼리로 전달하며, `ExtraFeeTemplateService::bulkDelete()`가 일괄 삭제 후 삭제 건수(`deleted_count`)를 반환합니다. 목록에서 여러 지역 할증 규칙을 선택해 한꺼번에 정리할 때 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/bulk + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.extra-fee-templates.bulk-store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ExtraFeeTemplateController@bulkStore` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| items | body | array | 예 | min 1, max 1000 | 처리 대상 항목 배열 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 CSV/엑셀 업로드로 추가배송비 템플릿을 한 번에 대량 등록합니다. `permission:sirsoft-ecommerce.shipping-policies.create` 권한이 필요하고 최대 1000건까지 `items` 배열로 전달하며, `ExtraFeeTemplateService::bulkCreate()`가 일괄 생성 후 등록 건수(`created_count`)를 201로 반환합니다. 도서산간 우편번호 목록처럼 다수의 지역 할증을 수기 입력 없이 파일로 업로드할 때 사용합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/bulk-toggle-active + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.extra-fee-templates.bulk-toggle-active` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ExtraFeeTemplateController@bulkToggleActive` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| is_active | body | boolean | 예 | — | 활성 여부 (true 활성 / false 비활성) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 여러 추가배송비 템플릿의 활성 상태를 한 번에 지정한 값(`is_active`)으로 변경합니다. `permission:sirsoft-ecommerce.shipping-policies.update` 권한이 필요하고 대상 ID 배열(`ids`)과 적용할 활성 여부를 전달하며, `ExtraFeeTemplateService::bulkToggleActive()`가 일괄 갱신 후 변경 건수(`updated_count`)를 반환합니다. 단건 토글과 달리 여러 지역 할증을 한꺼번에 켜거나 끌 때 사용합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.extra-fee-templates.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ExtraFeeTemplateController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 추가배송비 템플릿 1건을 삭제합니다. `permission:sirsoft-ecommerce.shipping-policies.delete` 권한이 필요하며, 대상이 없으면 404 를 반환하고 존재하면 `ExtraFeeTemplateService::delete()`가 삭제합니다. 특정 지역의 할증 규칙을 개별적으로 제거할 때 사용합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.extra-fee-templates.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ExtraFeeTemplateController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 추가배송비 템플릿 1건의 상세 정보를 조회합니다. `permission:sirsoft-ecommerce.shipping-policies.read` 권한이 필요하며, `ExtraFeeTemplateService::getDetail()`이 대상을 조회해 `ExtraFeeTemplateResource`로 반환합니다. 템플릿 편집 폼에 기존 값(우편번호·배송비·지역명·설명·활성 여부)을 채우는 용도로 사용되며, 대상이 없으면 404 를 반환합니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.extra-fee-templates.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ExtraFeeTemplateController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| zipcode | body | string | 예 | max 20 | 우편번호 | +| fee | body | number | 예 | min 0, max 9999999999.99 | 해당 우편번호에 부과할 추가 배송비 (0 이상) | +| region | body | string | 아니오 | max 100 | 지역/권역 | +| description | body | string | 아니오 | max 1000 | 설명 | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 추가배송비 템플릿 1건을 전체 수정합니다. `permission:sirsoft-ecommerce.shipping-policies.update` 권한이 필요하며, path 의 `id` 로 대상을 조회해 없으면 404(`messages.extra_fee_template.not_found`)를 반환합니다. 존재하면 `ExtraFeeTemplateService::update()`가 우편번호·추가 배송비·지역명·설명·활성 여부를 갱신합니다. 우편번호와 배송비는 필수이며, 수정된 템플릿이 활성 상태이면 이후 배송비 계산에 즉시 반영됩니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/{id}/toggle-active + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.extra-fee-templates.toggle-active` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ExtraFeeTemplateController@toggleActive` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 추가배송비 템플릿 1건의 활성 상태를 반전(active↔inactive)합니다. `permission:sirsoft-ecommerce.shipping-policies.update` 권한이 필요하며, path 의 `id` 로 대상을 조회해 없으면 404(`messages.extra_fee_template.not_found`)를 반환합니다. 존재하면 `ExtraFeeTemplateService::toggleActive()`가 현재 값을 뒤집어 저장하고 갱신된 템플릿을 반환합니다. 목록 화면에서 특정 지역 할증 규칙을 개별적으로 켜거나 끌 때 사용하며, 여러 건을 동일 값으로 일괄 설정하려면 bulk-toggle-active 엔드포인트를 사용합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/guest.md b/modules/_bundled/sirsoft-ecommerce/docs/api/guest.md new file mode 100644 index 00000000..d70a0949 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/guest.md @@ -0,0 +1,179 @@ +# Guest API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Guest 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### POST /api/modules/sirsoft-ecommerce/guest/orders/verify + +- **라우트명**: `api.modules.sirsoft-ecommerce.guest.orders.verify` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\OrderController@verify` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order_number | body | string | 예 | max 50 | 조회할 주문번호 (본인 확인 키 ①) | +| orderer_phone | body | string | 예 | max 20 | 주문자 전화번호 (본인 확인 키 ②) | +| guest_lookup_password | body | string | 예 | max 255 | 비회원 주문 조회 비밀번호 (본인 확인 키 ③) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 비회원이 주문번호·주문자 전화번호·조회 비밀번호로 본인 확인을 수행하고, 성공 시 30분 유효한 비회원 주문 조회 토큰을 발급받는 공개 엔드포인트입니다. 인증이 필요 없으며, `OrderController@verify`가 `GuestOrderAuthService::authenticate()`로 검증한 뒤 토큰과 최소 주문 요약(`order_number`, `order_status`)만 반환합니다. 주문 없음·회원 주문·전화번호 불일치·비밀번호 오류·잠금 등 모든 실패는 정보 노출 방지를 위해 동일한 404("주문을 찾을 수 없습니다")로 처리됩니다. 발급된 토큰은 이후 비회원 주문 상세/취소/환불 예상 등 후속 호출의 `X-Guest-Order-Token` 헤더로 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/guest/orders/{orderNumber}/cancel + +- **라우트명**: `api.modules.sirsoft-ecommerce.guest.orders.cancel` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\OrderController@cancel` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +| reason | body | string | 예 | — | 취소 사유 코드 (ClaimReason 의 refund 타입·활성·사용자 선택 가능 코드) | +| reason_detail | body | string | 아니오 | max 500 | 사용자 입력 취소 사유 상세 | +| items | body | array | 아니오 | min 1 | 처리 대상 항목 배열 (전달 시 부분취소, 미전달 시 전체취소) | +| refund_priority | body | string | 아니오 | `pg_first`, `points_first` | 환불 배분 우선순위 (PG 우선 / 포인트 우선, 미전달 시 pg_first) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 비회원이 조회 토큰으로 인증된 상태에서 자신의 주문을 취소합니다. 주문 소유권은 `VerifyGuestOrderToken` 미들웨어가 `X-Guest-Order-Token` 헤더로 검증하며, `OrderController@cancel`이 회원 취소와 동일한 `OrderCancellationService`를 재사용하되 취소자(`cancelledBy`)는 null로 둡니다. `items`를 전달하면 부분취소, 없으면 전체취소로 처리하고, `refund_priority`로 PG 환불과 포인트 환불 중 우선순위를 지정할 수 있습니다. 취소 후 갱신된 주문 상세를 `GuestOrderResource`로 반환합니다. + + +### POST /api/modules/sirsoft-ecommerce/guest/orders/{orderNumber}/estimate-refund + +- **라우트명**: `api.modules.sirsoft-ecommerce.guest.orders.estimate-refund` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\OrderController@estimateRefund` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +| items | body | array | 예 | min 1 | 처리 대상 항목 배열 | +| refund_priority | body | string | 아니오 | `pg_first`, `points_first` | 환불 배분 우선순위 (PG 우선 / 포인트 우선, 미전달 시 pg_first) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 비회원이 취소를 확정하기 전에 특정 옵션(`items`) 취소 시 예상 환불 금액을 미리 계산해 보여줍니다. 조회 토큰(`X-Guest-Order-Token`)으로 주문 소유권이 검증되며, `OrderController@estimateRefund`가 `OrderCancellationService::previewRefund()`로 실제 취소를 수행하지 않고 환불 예상값만 반환합니다. `refund_priority`에 따라 PG 우선/포인트 우선 환불 배분 결과가 달라집니다. 비회원 주문 취소 화면에서 "환불 예정 금액"을 미리 안내하는 용도입니다. + + +### POST /api/modules/sirsoft-ecommerce/guest/orders/{orderNumber}/options/{optionId}/confirm + +- **라우트명**: `api.modules.sirsoft-ecommerce.guest.orders.confirm-option` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\OrderController@confirmOption` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +| optionId | path | string | 예 | — | 대상 option의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 비회원이 배송 완료된 주문의 개별 옵션(`optionId`)을 구매확정합니다. 조회 토큰(`X-Guest-Order-Token`)으로 주문 소유권이 검증되며, `OrderController@confirmOption`이 토큰으로 검증된 주문에 실제 속한 옵션인지 다시 확인한 뒤 `OrderService::confirmOption()`을 호출합니다. 주문에 속하지 않은 옵션 ID면 404, 확정 불가 상태(배송 미완료 등)면 422를 반환합니다. 구매확정 시 적립 포인트 확정 등 후속 처리가 서비스 계층에서 이어집니다. + + +### PUT /api/modules/sirsoft-ecommerce/guest/orders/{orderNumber}/shipping-address + +- **라우트명**: `api.modules.sirsoft-ecommerce.guest.orders.update-shipping-address` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\OrderController@updateShippingAddress` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +| recipient_name | body | string | 예 | max 50 | 수령인 이름 | +| recipient_phone | body | string | 예 | max 20 | 수령인 연락처 | +| recipient_tel | body | string | 아니오 | max 20 | 수령인 일반전화 (선택 연락처) | +| country_code | body | string | 아니오 | — | 국가 코드 (ISO 3166-1 alpha-2) | +| zipcode | body | string | 아니오 | max 10 | 우편번호 | +| address | body | string | 아니오 | max 255 | 기본 주소 | +| address_detail | body | string | 아니오 | max 255 | 상세 주소 | +| address_line_1 | body | string | 아니오 | max 255 | 주소 1행 (기본 주소) | +| address_line_2 | body | string | 아니오 | max 255 | 주소 2행 (상세 주소) | +| intl_city | body | string | 아니오 | max 100 | 도시 (국제 주소) | +| intl_state | body | string | 아니오 | max 100 | 주/도 (국제 주소) | +| intl_postal_code | body | string | 아니오 | max 20 | 우편번호 (국제 주소) | +| delivery_memo | body | string | 아니오 | max 255 | 배송 메모 (배송 시 요청사항) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 비회원이 배송 전 상태의 주문 배송지를 수정합니다. 조회 토큰(`X-Guest-Order-Token`)으로 주문 소유권이 검증되며, 비회원은 저장된 회원 주소(`address_id`)를 쓸 수 없으므로 수취인·연락처·주소 필드를 직접 입력받아 `OrderController@updateShippingAddress`가 회원과 동일한 `OrderService::updateShippingAddress()`로 처리합니다. 국내(`zipcode`/`address`)와 해외(`address_line_1`·`intl_city` 등) 주소 필드를 함께 지원합니다. 이미 배송이 시작된 주문 등 수정 불가 상태면 422를 반환합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/inquiries.md b/modules/_bundled/sirsoft-ecommerce/docs/api/inquiries.md new file mode 100644 index 00000000..4274c3ab --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/inquiries.md @@ -0,0 +1,320 @@ +# Inquiries API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Inquiries 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### DELETE /api/modules/sirsoft-ecommerce/admin/inquiries/{inquiryId} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.inquiries.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductInquiryController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.inquiries.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.inquiries.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 상품 1:1 문의 1건을 삭제합니다. `sirsoft-ecommerce.inquiries.delete` 권한이 필요하며, `ProductInquiryService::deleteInquiry()` 가 해당 문의를 제거하고 `{deleted: true}` 를 반환합니다. 문의가 존재하지 않는 등 삭제 불가 상황에서는 서비스가 `RuntimeException` 을 던져 422 로 응답합니다. 관리자 문의 관리 화면에서 부적절하거나 중복된 문의를 정리할 때 사용합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/inquiries/{inquiryId}/reply + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.inquiries.reply.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductInquiryController@destroyReply` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.inquiries.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 문의에 등록된 답변을 삭제합니다. `sirsoft-ecommerce.inquiries.update` 권한이 필요하며, `ProductInquiryService::deleteReply()` 가 답변을 제거하고 문의를 미답변 상태로 되돌린 뒤 `{deleted: true}` 를 반환합니다. 문의 자체는 유지되며, 잘못 등록한 답변을 회수할 때 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/inquiries/{inquiryId}/reply + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.inquiries.reply` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductInquiryController@reply` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.inquiries.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | +| content | body | string | 예 | min 1, max 5000 | 본문 내용 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 상품 문의에 답변을 등록합니다. `sirsoft-ecommerce.inquiries.update` 권한이 필요하며, `ProductInquiryService::createReply()` 가 `content`(1~5000자)로 답변을 저장하고 문의를 답변완료 상태로 전환한 뒤 `{id, is_answered}` 를 201 로 반환합니다. 이미 답변이 있는 등 등록 불가 상황에서는 `RuntimeException` 이 던져져 422 로 응답합니다. 관리자가 고객 문의에 응대할 때 사용합니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/inquiries/{inquiryId}/reply + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.inquiries.reply.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductInquiryController@updateReply` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.inquiries.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | +| content | body | string | 예 | min 1, max 5000 | 본문 내용 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 문의에 이미 등록된 답변 내용을 수정합니다. `sirsoft-ecommerce.inquiries.update` 권한이 필요하며, `ProductInquiryService::updateReply()` 가 `content`(1~5000자)로 기존 답변을 갱신하고 `{id}` 를 반환합니다. 답변이 없는 문의 등 수정 불가 상황에서는 `RuntimeException` 이 던져져 422 로 응답합니다. 오탈자 정정 등 답변 내용을 고칠 때 사용합니다. + + +### GET /api/modules/sirsoft-ecommerce/user/inquiries + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.inquiries.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\ProductInquiryController@index` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `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) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 로그인 회원이 마이페이지에서 자신이 작성한 상품 문의 목록을 조회합니다. `auth:sanctum` 인증만 요구하며, `ProductInquiryService::getUserInquiries()` 가 로그인 사용자(`Auth::id()`)의 문의를 `search`(검색어)·`is_answered`(답변 여부) 필터와 `per_page`(기본 10)로 페이지네이션해 `items`(문의 배열)와 `meta`(페이지 정보)로 반환합니다. 마이페이지 문의 내역 화면을 채우는 데 사용합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/user/inquiries/{inquiryId} + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.inquiries.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\ProductInquiryController@destroy` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 로그인 회원이 자신이 작성한 문의를 삭제합니다. `auth:sanctum` 인증이 필요하며, 컨트롤러가 문의를 조회해 없으면 404, `inquiry->user_id` 가 로그인 사용자와 다르면 403 을 반환한 뒤 `ProductInquiryService::deleteInquiry()` 로 삭제하고 `{deleted: true}` 를 반환합니다. 마이페이지에서 본인 문의를 취소/삭제할 때 사용합니다. + + +### PUT /api/modules/sirsoft-ecommerce/user/inquiries/{inquiryId} + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.inquiries.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\ProductInquiryController@update` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | +| title | body | string | 아니오 | — | 제목 | +| category | body | string | 아니오 | — | 문의 분류 (게시판 설정 기반 유형 슬러그, 연동 게시판 Post 로 저장) | +| content | body | string | 예 | — | 본문 내용 | +| is_secret | body | boolean | 아니오 | — | secret 여부 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.inquiry.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 로그인 회원이 자신이 작성한 문의 내용을 수정합니다. `auth:sanctum` 인증이 필요하며, 컨트롤러가 문의를 조회해 없으면 404, `inquiry->user_id` 가 로그인 사용자와 다르면 403 을 반환한 뒤 `ProductInquiryService::updateInquiry()` 로 제목·분류·본문·비밀글 여부를 갱신하고 `{id}` 를 반환합니다. `content` 는 필수이며, 마이페이지에서 아직 답변되지 않은 본인 문의를 고칠 때 사용합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/user/inquiries/{inquiryId}/reply + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.inquiries.reply.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\ProductInquiryController@destroyReply` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 사용자 표면에서 문의 답변 권한을 가진 회원이 문의 답변을 삭제합니다. `auth:sanctum` 인증에 더해 컨트롤러가 `PermissionHelper::check('sirsoft-ecommerce.inquiries.update')` 로 답변 권한을 확인(없으면 403)한 뒤 `ProductInquiryService::deleteReply()` 로 답변을 제거하고 `{deleted: true}` 를 반환합니다. 답변 권한을 위임받은 사용자(예: 상담원 역할)가 사용자 화면에서 답변을 회수할 때 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/user/inquiries/{inquiryId}/reply + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.inquiries.reply` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\ProductInquiryController@reply` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | +| content | body | string | 예 | min 1, max 5000 | 본문 내용 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 사용자 표면에서 문의 답변 권한을 가진 회원이 문의에 답변을 등록합니다. `auth:sanctum` 인증에 더해 컨트롤러가 `PermissionHelper::check('sirsoft-ecommerce.inquiries.update')` 로 답변 권한을 확인(없으면 403)한 뒤 `ProductInquiryService::createReply()` 가 `content`(1~5000자)로 답변을 저장하고 문의를 답변완료로 전환해 `{id, is_answered}` 를 201 로 반환합니다. 관리자 화면이 아닌 사용자 프론트에서 답변을 처리하는 상담원 역할에 사용합니다. + + +### PUT /api/modules/sirsoft-ecommerce/user/inquiries/{inquiryId}/reply + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.inquiries.reply.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\ProductInquiryController@updateReply` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 | +| content | body | string | 예 | min 1, max 5000 | 본문 내용 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 사용자 표면에서 문의 답변 권한을 가진 회원이 기존 문의 답변을 수정합니다. `auth:sanctum` 인증에 더해 컨트롤러가 `PermissionHelper::check('sirsoft-ecommerce.inquiries.update')` 로 답변 권한을 확인(없으면 403)한 뒤 `ProductInquiryService::updateReply()` 가 `content`(1~5000자)로 답변을 갱신하고 `{id}` 를 반환합니다. 사용자 프론트에서 답변을 처리하는 상담원 역할이 답변 내용을 정정할 때 사용합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/mileage-transactions.md b/modules/_bundled/sirsoft-ecommerce/docs/api/mileage-transactions.md new file mode 100644 index 00000000..1eeaea09 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/mileage-transactions.md @@ -0,0 +1,226 @@ +# Mileage Transactions API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Mileage Transactions 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/mileage-transactions + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.mileage-transactions.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\MileageTransactionController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.mileage.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| sort | query | string | 아니오 | `created_at_desc`, `created_at_asc`, `amount_desc`, `amount_asc` | 정렬 기준 (필드명, `-` 접두 시 내림차순) | +| search_field | query | string | 아니오 | `member`, `member_id`, `email`, `order` | 검색 대상 필드명 (검색어를 적용할 컬럼) | +| search_keyword | query | string | 아니오 | max 100 | 검색 키워드 (부분 일치) | +| type | query | string | 아니오 | `earn`, `use`, `expire`, `adjust` | 유형 필터 (해당 유형의 항목만 조회) | +| currency | query | string | 아니오 | max 10 | 통화 코드 필터 (해당 통화로 기록된 거래만 조회) | +| start_date | query | date | 아니오 | — | 조회 기간 시작일 (이 날짜 이후 데이터) | +| end_date | query | date | 아니오 | — | 조회 기간 종료일 (이 날짜 이전 데이터) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `68` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `528` | 기본 키 (내부 식별자) | +| user_id | integer | `166` | user 식별자 (연관 리소스 참조) | +| currency | string | `KRW` | 거래 기록 통화 코드 (주문 기준통화 스냅샷, 금액 표기·잔액 집계 단위) | +| type | string | `admin_earn` | 거래 유형 (MileageTransactionTypeEnum 8종: purchase_earn·admin_earn·order_use·admin_deduct·expired·refund_restore·order_cancel_restore·earn_cancel) | +| type_label | string | `관리자 지급` | `type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| admin_badge_group | string | `amber` | 관리자 내역 화면 배지 색상 그룹 (적립계=green·사용계=blue·소멸=gray·복원계=teal·수동/회수계=amber) | +| user_display_category | string | `adjust` | 회원 마이페이지 표시용 4분류 (earn·use·expire·adjust — 복원·수동·회수는 adjust 로 통합) | +| amount | integer | `1000` | 거래 금액 (양수=적립, 음수=차감) | +| amount_formatted | string | `1,000원` | `amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| remaining_amount | integer | `0` | 잔여 금액 (적립건만 양수, FIFO 차감으로 소진 — 미만료 잔여 합이 잔액 SSoT) | +| remaining_amount_formatted | string | `0원` | `remaining_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| balance_after | integer | `1000` | 거래 직후 잔액 (감사용 스냅샷, 베스트에포트) | +| order_id | integer | `436` | order 식별자 (연관 리소스 참조) | +| order_option_id | integer | `824` | order option 식별자 (연관 리소스 참조) | +| order_cancel_id | integer | `18` | order cancel 식별자 (연관 리소스 참조) | +| source_transaction_id | integer | `523` | source transaction 식별자 (연관 리소스 참조) | +| granted_by | integer | `1` | 부여 주체 식별자 (NULL=시스템 자동, user ID=관리자 수동 부여) | +| granted_by_name | string | `관리자` | 부여 관리자 이름 (grantedByUser 관계 eager load 시에만 노출) | +| granted_by_uuid | string | `a1e0a91a-fba6-491c-a53e-7285a5686857` | 부여 관리자 UUID (grantedByUser 관계 eager load 시에만 노출) | +| user_name | string | `API 문서 샘플 사용자` | 거래 대상 회원 이름 (user 관계 eager load 시에만 노출) | +| user_uuid | string | `a231747f-e82e-4cf2-9ae1-a261849dce40` | 거래 대상 회원 UUID (user 관계 eager load 시에만 노출) | +| order_number | string | `20260619-1425382147` | 연관 주문번호 (order 관계 eager load 시에만 노출) | +| description | string | `마일리지 적립 (660원)` | 설명 (다국어 필드는 로케일별 값 객체) | +| memo | string | `111` | 관리자 메모 (수동 지급/차감·적립건 편집 시 입력) | +| expires_at | string | `2027-07-02T15:29:30+00:00` | expires 일시 | +| expires_at_formatted | string | `2027-07-03 00:29:30` | `expires_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| expires_at_date | string | `2027-07-03` | `expires_at` 의 사이트 타임존 기준 날짜 부분 (시각 제외) | +| expired_at | null | `null` | expired 일시 | +| expired_at_formatted | null | `null` | `expired_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| created_at | string | `2026-07-07T05:47:31+00:00` | 생성 일시 | +| created_at_formatted | string | `2026-07-07 14:47:31` | `created_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| created_at_date | string | `2026-07-07` | `created_at` 의 사이트 타임존 기준 날짜 부분 (시각 제외) | +| is_earning | boolean | `true` | earning 여부 | +| can_edit_expiry | boolean | `false` | edit expiry 수행 가능 여부 (권한 기반) | +| expired_amount | integer | `0` | 이 적립 lot 을 source 로 소멸(expired)된 금액 합계 (목록 조회 시 eager 집계, 단건 조회 시 0 폴백) | +| expired_amount_formatted | string | `0원` | `expired_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| expiry_state | string | `active` | 적립건 소멸 상태 (active=미소멸, partial_expired=일부 소멸, fully_expired=전액 소멸 — 적립계만 의미) | +| abilities | object | `{"can_manage":true,"can_edit":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.mileage.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 전체 회원의 마일리지 거래 원장을 검색·필터·페이지네이션으로 조회합니다. `permission:sirsoft-ecommerce.mileage.read` 권한이 필요하며, 회원(member·member_id·email)·주문번호 검색, type(earn·use·expire·adjust)·통화·기간 필터, 금액/생성일 정렬을 지원합니다. `UserMileageService::paginateAdminHistory()`가 조회하고 `MileageTransactionCollection`이 통화 필터 후보(`withCurrencies`)와 함께 직렬화합니다. 각 행은 원장 스냅샷(balance_after, remaining_amount 등)을 그대로 노출하며 원장은 불변입니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/mileage-transactions + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.mileage-transactions.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\MileageTransactionController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.mileage.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| user_id | body | uuid | 예 | — | user 식별자 | +| action | body | string | 예 | `earn`, `deduct` | 처리 동작 (earn=수동 적립 → adminEarn, deduct=수동 차감 → adminDeduct FIFO 소진) | +| amount | body | integer | 예 | min 1 | 지급/차감할 마일리지 금액 (양수) | +| currency | body | string | 예 | max 10 | 대상 통화 코드 (해당 통화 잔액에 적용) | +| memo | body | string | 아니오 | max 1000 | 관리자 메모 (거래에 기록) | +| description | body | string | 아니오 | max 500 | 설명 | +| expires_at | body | date | 아니오 | — | 만료일 직접 지정 (지급 시 `use_default_expiry`=false 인 경우 적용) | +| use_default_expiry | body | boolean | 아니오 | — | 지급 시 정책 기본 만료일 적용 여부 (기본 true, false 면 `expires_at` 사용) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.mileage.manage`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 특정 회원에게 마일리지를 수동으로 지급하거나 차감합니다. `permission:sirsoft-ecommerce.mileage.manage` 권한이 필요하고 회원은 `user_id`(uuid)로 지정하며, `action`(earn/deduct)에 따라 `UserMileageService::adminEarn()` 또는 `adminDeduct()`를 호출합니다. 지급 시 `use_default_expiry`(기본 true)면 정책 기본 만료일을, 아니면 `expires_at`을 적용하고, 차감은 FIFO로 적립건에서 소진합니다. 잔액 부족 등 도메인 규칙 위반은 `MileageValidationException`으로 422를 반환합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/mileage-transactions/extend-expiry + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.mileage-transactions.extend-expiry` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\MileageTransactionController@extendExpiry` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.mileage.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| user_id | body | uuid | 예 | — | user 식별자 | +| lot_ids | body | array | 예 | min 1 | lot 식별자 배열 | +| days | body | integer | 예 | min 1, max 3650 | 각 lot 만료일을 연장할 일수 (최대 3650일) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.mileage.manage`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 특정 회원의 여러 적립 lot 의 유효기간을 한 번에 연장합니다. `permission:sirsoft-ecommerce.mileage.manage` 권한이 필요하며, 회원(`user_id` uuid)과 대상 적립건 배열(`lot_ids`), 연장 일수(`days`, 최대 3650)를 받아 `UserMileageService::extendLotExpiry()`가 각 lot 의 만료일을 연장하고 실제로 연장된 건수(`extended_count`)를 반환합니다. 이미 소멸/사용된 lot 등 대상 외 건은 서비스가 걸러내므로 반환 건수가 요청한 `lot_ids` 수보다 작을 수 있습니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/mileage-transactions/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.mileage-transactions.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\MileageTransactionController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.mileage.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| memo | body | string | 아니오 | max 1000 | 관리자 메모 보정값 (요청에 포함된 경우만 갱신, 빈 값으로 비우기 허용) | +| expires_at | body | date | 아니오 | — | expires 일시 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.mileage.manage`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 기존 적립 거래의 부가 필드(관리자 메모 `memo`, 만료일 `expires_at`)만 보정합니다. 마일리지 원장은 불변이므로 금액·유형 등 핵심 값은 수정할 수 없고, 요청에 실제로 포함된 키만 갱신합니다(`memo` 만 보내면 만료일은 유지). `UserMileageService::updateAdminTransaction()`가 적립계 거래인지·소멸/사용된 lot 이 아닌지 검증하며, 적립계 외 거래 등 규칙 위반은 `MileageValidationException`(422)으로 거부합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/mileage-transactions/{id}/linked + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.mileage-transactions.linked` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\MileageTransactionController@linked` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.mileage.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.mileage.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 마일리지 내역 한 행을 펼쳤을 때 그와 연결된 거래들을 조회합니다. `permission:sirsoft-ecommerce.mileage.read` 권한이 필요하며, `UserMileageService::getLinkedTransactions()`가 적립건이면 그 적립을 FIFO 로 소비한 차감 거래들을, 차감/복원건이면 원본 적립·취소 연결 거래를 찾아 `MileageTransactionCollection`으로 반환합니다. 해당 거래가 없으면 404 를 반환하며, 연결 거래가 없으면 빈 목록이 내려옵니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/mileage.md b/modules/_bundled/sirsoft-ecommerce/docs/api/mileage.md new file mode 100644 index 00000000..366f28be --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/mileage.md @@ -0,0 +1,112 @@ +# Mileage API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Mileage 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/user/mileage + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.mileage.balance` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserMileageController@balance` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| mileage | object | `{"enabled":false,"available":12910,"pending":14000,"expir…` | 마일리지 잔액 요약 객체 (enabled 기능 활성화 여부, available 사용 가능, pending 적립 대기, expiring_soon/expiring_date 소멸 예정, total_earned/total_used 누적 적립·사용, by_currency 통화별 잔액) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 로그인한 회원이 마이페이지에서 자신의 마일리지 잔액 요약을 조회합니다. `auth:sanctum` 인증만 필요하며, `UserMileageService::getBalance()`가 마일리지 기능 활성화 여부, 사용 가능(available)·적립 대기(pending)·소멸 예정 금액을 계산해 `mileage` 객체로 반환합니다. 마일리지 기능이 꺼져 있으면 `enabled: false` 와 0값이 내려오므로 화면에서 잔액 위젯 노출 여부를 이 플래그로 판단할 수 있습니다. + + +### GET /api/modules/sirsoft-ecommerce/user/mileage/history + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.mileage.history` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserMileageController@history` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| category | query | string | 아니오 | `earn`, `use`, `expire`, `adjust` | 분류 필터 (해당 분류의 항목만 조회) | +| currency | query | string | 아니오 | max 10 | 통화 코드 필터 (해당 통화의 마일리지 거래만 조회) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| transactions | object | `{"data":[{"number":6,"id":527,"user_id":1,"currency":"KRW…` | 마일리지 거래 내역 페이지네이션 객체 (`data` 거래 항목 배열 + 페이지 메타, `MileageTransactionCollection` 으로 직렬화) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 로그인한 회원이 마이페이지에서 자신의 마일리지 적립/사용 내역을 페이지네이션으로 조회합니다. `category`(earn·use·expire·adjust) 4분류 필터와 `currency`·`per_page`(최대 100)를 지원하며, `UserMileageService::paginateUserHistory()`가 필터를 적용해 조회한 뒤 `MileageTransactionCollection`으로 직렬화해 `transactions` 에 담습니다. `category` 는 원장의 원시 type 이 아니라 사용자 표시용 4분류로 매핑된 값입니다. + + +### GET /api/modules/sirsoft-ecommerce/user/mileage/max-usable + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.mileage.max-usable` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserMileageController@maxUsable` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order_amount | query | integer | 예 | min 0 | 사용 가능 상한 계산 기준 주문금액 (마일리지 사용액은 이 금액을 넘을 수 없음) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.mileage.max_usable_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 체크아웃 화면에서 특정 주문금액(`order_amount`)에 실제로 사용 가능한 최대 마일리지를 계산해 반환합니다. `auth:sanctum` 인증이 필요하고 `order_amount` 는 필수이며, `UserMileageService::getMaxUsable()`가 보유 잔액·최소 사용 정책·주문금액 상한을 종합해 사용 가능 상한을 산출하고 현재 잔액(available)도 함께 내려줍니다. 확장은 `sirsoft-ecommerce.mileage.max_usable_validation_rules` 필터로 검증 파라미터를 추가할 수 있습니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/options.md b/modules/_bundled/sirsoft-ecommerce/docs/api/options.md new file mode 100644 index 00000000..5292df17 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/options.md @@ -0,0 +1,121 @@ +# Options API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Options 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### PATCH /api/modules/sirsoft-ecommerce/admin/options/bulk-price + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.options.bulk-price` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductOptionController@bulkUpdatePrice` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| product_ids | body | array | 아니오 | — | product 식별자 배열 | +| option_ids | body | array | 아니오 | — | option 식별자 배열 | +| method | body | string | 예 | `increase`, `decrease`, `fixed` | 가격 변경 방식 (increase 인상 / decrease 인하 / fixed 고정가로 설정) | +| value | body | number | 예 | min 0 | 값 | +| unit | body | string | 예 | `won`, `percent` | 변경 단위 (won 금액 기준 / percent 비율 기준) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product_option.bulk_price_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 선택한 상품/옵션의 판매가를 일괄 변경합니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, `ProductOptionService::bulkUpdatePriceByMixedIds()`가 처리합니다. `product_ids`는 해당 상품의 모든 옵션을, `option_ids`는 "productId-optionId" 형식으로 개별 선택된 옵션을 대상으로 합니다. `method`(increase/decrease/fixed)와 `unit`(won/percent) 조합으로 인상·인하·고정가를 적용하며, 검증 실패 시 422, 그 외 오류 시 500을 반환합니다. `sirsoft-ecommerce.product_option.bulk_price_validation_rules` 필터로 확장이 검증 규칙을 추가할 수 있습니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/options/bulk-stock + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.options.bulk-stock` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductOptionController@bulkUpdateStock` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| product_ids | body | array | 아니오 | — | product 식별자 배열 | +| option_ids | body | array | 아니오 | — | option 식별자 배열 | +| method | body | string | 예 | `increase`, `decrease`, `set` | 재고 변경 방식 (increase 증가 / decrease 감소 / set 특정 수량으로 설정) | +| value | body | integer | 예 | min 0 | 값 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product_option.bulk_stock_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 선택한 상품/옵션의 재고를 일괄 변경합니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, `ProductOptionService::bulkUpdateStockByMixedIds()`가 처리합니다. `product_ids`는 해당 상품의 모든 옵션을, `option_ids`는 "productId-optionId" 형식으로 개별 선택된 옵션을 대상으로 합니다. `method`(increase/decrease/set)와 정수 `value`로 재고를 가감하거나 특정 수량으로 설정하며, 검증 실패 시 422, 그 외 오류 시 500을 반환합니다. `sirsoft-ecommerce.product_option.bulk_stock_validation_rules` 필터로 확장이 검증 규칙을 추가할 수 있습니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/options/bulk-update + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.options.bulk-update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductOptionController@bulkUpdate` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| bulk_changes | body | array | 아니오 | — | 옵션 일괄 변경 조건 (`price_adjustment`/`stock_quantity` 각각 method+value, 설정된 필드가 개별 수정보다 우선 적용) | +| items | body | array | 아니오 | — | 처리 대상 항목 배열 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.option.bulk_update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 옵션들을 통합 일괄 업데이트합니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, 상품은 미선택하고 옵션만 선택된 경우에 사용됩니다. `ids`(대상 옵션, 최소 1개)와 함께 `bulk_changes`(일괄 변경 조건)와 `items`(개별 인라인 수정)를 받아 `ProductOptionService::bulkUpdate()`가 처리하며, 일괄 변경 조건이 설정된 필드가 우선 적용되고 나머지는 개별 수정이 반영됩니다. 검증 실패 시 422, 그 외 오류 시 500을 반환하고, `sirsoft-ecommerce.option.bulk_update_validation_rules` 필터로 확장이 검증 규칙을 추가할 수 있습니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/orders.md b/modules/_bundled/sirsoft-ecommerce/docs/api/orders.md new file mode 100644 index 00000000..fb796245 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/orders.md @@ -0,0 +1,935 @@ +# Orders API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Orders 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/orders + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.orders.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\OrderController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| search_field | query | string | 아니오 | `all`, `order_number`, `orderer_name`, `recipient_name`, `orderer_phone`, `recipient_phone`, `product_name`, `sku` | 검색 대상 필드명 (검색어를 적용할 컬럼) | +| search_keyword | query | string | 아니오 | max 200 | 검색 키워드 (부분 일치) | +| date_type | query | string | 아니오 | — | 기간 필터 기준 일자 종류 (ordered_at 주문일 / paid_at 결제일 / confirmed_at 구매확정일 / delivered_at 배송완료일 / cancelled_at 취소일) | +| start_date | query | date | 아니오 | — | 조회 기간 시작일 (이 날짜 이후 데이터) | +| end_date | query | date | 아니오 | — | 조회 기간 종료일 (이 날짜 이전 데이터) | +| order_status | query | array | 아니오 | — | 주문상태 다중 선택 필터 (OrderStatusEnum 값 배열, 해당 상태의 주문만 조회) | +| option_status | query | array | 아니오 | — | 주문옵션 상태 다중 선택 필터 (OrderStatusEnum 값 배열, 해당 옵션 상태를 가진 주문만 조회) | +| shipping_type | query | array | 아니오 | — | 배송유형 다중 선택 필터 (ShippingType 코드 배열) | +| payment_method | query | array | 아니오 | — | 결제수단 다중 선택 필터 (PaymentMethodEnum 값 배열) | +| category_id | query | integer | 아니오 | — | category 식별자 | +| min_amount | query | integer | 아니오 | min 0 | 주문금액 범위 필터 하한 (이 금액 이상 주문만 조회) | +| max_amount | query | integer | 아니오 | min 0 | 주문금액 범위 필터 상한 (이 금액 이하 주문만 조회) | +| country_codes | query | array | 아니오 | — | 배송국가 코드 다중 선택 필터 (ISO 3166-1 alpha-2 2자리 코드 배열) | +| order_device | query | array | 아니오 | — | 주문 디바이스 다중 선택 필터 (DeviceTypeEnum 값 배열 — pc/mobile/app 등) | +| min_shipping_amount | query | integer | 아니오 | min 0 | 배송비 범위 필터 하한 (이 배송비 이상 주문만 조회) | +| max_shipping_amount | query | integer | 아니오 | min 0 | 배송비 범위 필터 상한 (이 배송비 이하 주문만 조회) | +| shipping_policy_id | query | integer | 아니오 | — | shipping policy 식별자 | +| user_id | query | integer | 아니오 | — | user 식별자 | +| orderer_uuid | query | uuid | 아니오 | — | 특정 회원의 주문만 조회하는 주문자 UUID 필터 (회원 검색 연동용) | +| member_type | query | string | 아니오 | `member`, `guest` | 회원 구분 필터 (member 회원 주문 / guest 비회원 주문) | +| sort_by | query | string | 아니오 | `ordered_at`, `paid_at`, `total_amount` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| per_page | query | integer | 아니오 | min 10, max 100 | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.order.list_validation_rules`, `sirsoft-ecommerce.order.list_validation_messages`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `128` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `455` | 기본 키 (내부 식별자) | +| order_number | string | `ORD-20260707-000002` | 주문번호 (사용자 노출용 고유 식별 코드) | +| order_status | string | `pending_payment` | 주문상태 (OrderStatusEnum 값 — 결제대기/결제완료/배송중 등) | +| order_status_label | string | `결제대기` | `order_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| order_status_variant | string | `warning` | `order_status` 값의 표시 변형 키 (UI 배지 색상/스타일) | +| base_currency | string | `KRW` | 금액 표기 기준 통화 (모든 *_formatted 필드의 통화, 주문 시점 base_currency 고정) | +| payment_currency | string | `KRW` | 결제 통화 (유저가 선택·결제한 통화, base_currency 와 다르면 병기 표시) | +| is_cross_currency | boolean | `false` | cross currency 여부 | +| is_partially_cancelled | boolean | `false` | partially cancelled 여부 | +| total_amount | integer | `193397` | 최종 주문금액 (상품합계 − 할인 + 배송비) | +| total_amount_formatted | string | `193,397원` | `total_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_shipping_amount | integer | `0` | 총 배송비 | +| total_shipping_amount_formatted | string | `0원` | `total_shipping_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_paid_amount | integer | `0` | 총 실제 결제금액 (PG 결제된 금액) | +| total_paid_amount_formatted | string | `0원` | `total_paid_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_unpaid_amount | integer | `193397` | 미결제 잔액 (최종 주문금액 − 실제 결제금액) | +| total_unpaid_amount_formatted | string | `193,397원` | `total_unpaid_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_cancelled_amount | integer | `0` | 총 취소금액 | +| total_refunded_amount | integer | `0` | 총 환불금액 | +| total_points_used_amount | integer | `0` | 총 포인트(마일리지) 사용액 | +| total_points_used_amount_formatted | string | `0원` | `total_points_used_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_earned_points_amount | integer | `1934` | 총 적립 예정 포인트 | +| total_earned_points_amount_formatted | string | `1,934원` | `total_earned_points_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| ordered_at | string | `2026-07-07T05:47:30+00:00` | ordered 일시 | +| ordered_at_formatted | string | `2026-07-07 14:47:30` | `ordered_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| order_device | string | `pc` | 주문 디바이스 (DeviceTypeEnum 값 — pc/mobile/app) | +| order_device_label | string | `PC` | `order_device` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| is_first_order | boolean | `true` | first order 여부 | +| user | object | `{"uuid":"a23317ba-05bc-4de3-8272-2b73c091a266","name":"남상준"}` | 회원 주문의 주문자 요약 (uuid·name, 비회원 주문이면 미포함) | +| first_option | object | `{"product_name":"quisquam et quia","product_option_name":…` | 대표 표시용 첫 번째 주문 옵션 요약 (상품명·옵션명·수량·썸네일·추가옵션 요약) | +| options_count | integer | `1` | options 개수 (집계) | +| address | object | `{"orderer_name":"관리자","recipient_name":"구태호","recipient_c…` | 배송지 요약 (주문자명·수령인명·배송국가 코드/현지화명) | +| payment | object | `{"payment_method":"dbank","payment_method_label":"무통장입금"}` | 결제 요약 (결제수단 값·현지화 라벨) | +| shipping | object | `{"shipping_type":null,"shipping_type_label":null,"shippin…` | 배송 요약 (배송유형·배송방법 라벨·택배사명·송장번호, 첫 번째 배송 기준) | +| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 주문을 다양한 필터·검색·정렬 조건으로 페이지네이션 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.orders.read` 권한이 필요하며, `Admin\OrderController@index`가 `OrderService::getList()`로 목록을, `getStatistics()`로 상태별 통계를 함께 가져와 `OrderCollection`에 담아 반환합니다. 검색 필드(주문번호/주문자명/상품명/SKU 등)·기간·주문상태·결제수단·금액대·회원/비회원 구분 등 폭넓은 필터를 지원합니다. 관리자 주문 목록 화면의 기본 데이터 소스입니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/orders/bulk + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.orders.bulk` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\OrderController@bulkUpdate` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| order_status | body | string | 아니오 | — | 일괄 전환할 주문상태 (OrderStatusEnum 값, pending_order 제외 · 전이 규칙 검증) | +| carrier_id | body | integer | 아니오 | — | carrier 식별자 | +| tracking_number | body | string | 아니오 | max 50 | 송장(운송장)번호 (배송 관련 상태로 전환 시 carrier_id 와 함께 필수) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 여러 주문(`ids`)의 주문상태나 배송 정보(택배사·송장번호)를 일괄 변경합니다. `auth:sanctum` + `sirsoft-ecommerce.orders.update` 권한이 필요하며, `Admin\OrderController@bulkUpdate`가 `OrderService::bulkUpdate()`로 처리합니다. 주문 목록에서 여러 건을 선택해 "배송 처리"·"상태 일괄 변경" 등을 수행할 때 사용합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/orders/{order} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.orders.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\OrderController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | path | string | 예 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 특정 주문(`order`)을 소프트 삭제합니다. `auth:sanctum` + `sirsoft-ecommerce.orders.delete` 권한이 필요하며, `Admin\OrderController@destroy`가 `OrderService::delete()`를 호출합니다. 물리 삭제가 아닌 소프트 삭제(deleted_at 표시)이므로 데이터는 보존되며, 주문 목록/상세에서 제외됩니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/orders/{order} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.orders.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\OrderController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | path | string | 예 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `322` | 기본 키 (내부 식별자) | +| order_number | string | `20260617-0207256237` | 주문번호 | +| base_currency | string | `KRW` | 금액 표기 기준 통화 (모든 *_formatted 필드의 통화, 주문 시점 base_currency 고정) | +| payment_currency | string | `KRW` | 결제 통화 (유저가 선택·결제한 통화, base_currency 와 다르면 병기 표시) | +| is_cross_currency | boolean | `false` | cross currency 여부 | +| order_status | string | `payment_complete` | 주문상태 (OrderStatusEnum) | +| order_status_label | string | `결제완료` | `order_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| order_status_variant | string | `info` | `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 | `true` | first order 여부 | +| subtotal_amount | integer | `140000` | 상품 합계 (할인 전, 상품가×수량 합계) | +| subtotal_amount_formatted | string | `140,000원` | `subtotal_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_discount_amount | integer | `25000` | 총 할인금액 (모든 할인 합계) | +| total_discount_amount_formatted | string | `25,000원` | `total_discount_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_shipping_amount | integer | `0` | 총 배송비 | +| total_shipping_amount_formatted | string | `0원` | `total_shipping_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_amount | integer | `115000` | 최종 주문금액 (subtotal - discount + shipping) | +| total_amount_formatted | string | `115,000원` | `total_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_paid_amount | integer | `115000` | 총 실제 결제금액 (PG 결제액) | +| total_paid_amount_formatted | string | `115,000원` | `total_paid_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_due_amount | integer | `0` | 총 결제예정금액 (무통장 등) | +| total_due_amount_formatted | string | `0원` | `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 | `25000` | 주문 쿠폰 할인 합계 | +| total_order_coupon_discount_amount_formatted | string | `25,000원` | `total_order_coupon_discount_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_coupon_discount_amount | integer | `25000` | 총 쿠폰 할인금액 | +| total_coupon_discount_amount_formatted | string | `25,000원` | `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 | `1150` | 총 적립 예정 포인트 | +| total_earned_points_amount_formatted | string | `1,150원` | `total_earned_points_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| mc_subtotal_amount | object | `{"KRW":{"amount":140000,"formatted":"140,000원"},"USD":{"a…` | 상품합계 다중 통화 | +| mc_total_discount_amount | object | `{"KRW":{"amount":25000,"formatted":"25,000원"},"USD":{"amo…` | 총 할인 다중 통화 | +| mc_total_shipping_amount | object | `{"KRW":{"amount":0,"formatted":"0원"},"USD":{"amount":0,"f…` | 총 배송비 다중 통화 | +| mc_total_amount | object | `{"KRW":{"amount":115000,"formatted":"115,000원"},"USD":{"a…` | 최종금액 다중 통화 (payment_amount) | +| mc_total_product_coupon_discount_amount | object | `{"KRW":{"amount":0,"formatted":"0원"},"USD":{"amount":0,"f…` | 상품 쿠폰 할인 다중 통화 | +| mc_total_order_coupon_discount_amount | object | `{"KRW":{"amount":25000,"formatted":"25,000원"},"USD":{"amo…` | 주문 쿠폰 할인 다중 통화 | +| mc_total_coupon_discount_amount | object | `{"KRW":{"amount":25000,"formatted":"25,000원"},"USD":{"amo…` | 쿠폰 할인 합계 다중 통화 | +| mc_total_code_discount_amount | object | `{"KRW":{"amount":0,"formatted":"0원"},"USD":{"amount":0,"f…` | 할인코드 할인 다중 통화 | +| mc_total_points_used_amount | object | `{"KRW":{"amount":0,"formatted":"0원"},"USD":{"amount":0,"f…` | 포인트 사용 다중 통화 | +| mc_total_deposit_used_amount | object | `{"KRW":{"amount":0,"formatted":"0원"},"USD":{"amount":0,"f…` | 예치금 사용 다중 통화 | +| item_count | integer | `2` | item 개수 (집계) | +| total_quantity | integer | `5` | 주문 옵션 수량 합계 (options 로드 시) | +| total_list_price | integer | `177000` | 정가 합계 (옵션 스냅샷 정가 × 수량 합계) | +| total_list_price_formatted | string | `177,000원` | `total_list_price` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| ordered_at | string | `2026-06-15T02:07:25+00:00` | ordered 일시 | +| ordered_at_formatted | string | `2026-06-15 11:07:25` | `ordered_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| paid_at | string | `2026-06-17T02:07:25+00:00` | paid 일시 | +| paid_at_formatted | string | `2026-06-17 11:07:25` | `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 | `10455` | 총 과세금액 | +| total_tax_amount_formatted | string | `10,455원` | `total_tax_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_vat_amount | integer | `0` | 총 부가세금액 | +| total_vat_amount_formatted | string | `0원` | `total_vat_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_taxable_supply_amount | integer | `10455` | 과세 공급가액 (총 과세금액 − 부가세, 영수증 과세금액 표시 SSoT) | +| total_taxable_supply_amount_formatted | string | `10,455원` | `total_taxable_supply_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_tax_free_amount | integer | `0` | 총 면세금액 | +| total_tax_free_amount_formatted | string | `0원` | `total_tax_free_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| user | object | `{"uuid":"a20683c6-14f8-4061-baa9-157c45e9de5a","name":"Jo…` | 회원 주문의 주문자 정보 (uuid·name·email, user 관계 로드 시 · 비회원이면 미포함) | +| user_id | string | `a20683c6-14f8-4061-baa9-157c45e9de5a` | user 식별자 (연관 리소스 참조) | +| user_login_id | null | `null` | 회원 로그인 아이디 (login_id, 비회원 주문이면 null) | +| orderer_name | string | `설창용` | 주문자 이름 (배송지에서 플래튼) | +| orderer_phone | string | `010-0650-9192` | 주문자 휴대전화 (배송지에서 플래튼) | +| orderer_tel | null | `null` | 주문자 일반전화 (배송지에서 플래튼, 미입력 시 null) | +| orderer_email | string | `moonchang.shim@gmail.com` | 주문자 이메일 (배송지에서 플래튼, 비회원 알림 수신 통로) | +| recipient_name | string | `길준` | 수령인 이름 (배송지에서 플래튼) | +| recipient_phone | string | `010-1612-1979` | 수령인 휴대전화 (배송지에서 플래튼) | +| recipient_tel | null | `null` | 수령인 일반전화 (배송지에서 플래튼, 미입력 시 null) | +| recipient_zipcode | string | `19882` | 수령인 우편번호 (배송지에서 플래튼) | +| recipient_address | string | `부산광역시 도봉구 역삼로 201` | 수령인 기본 주소 (배송지에서 플래튼) | +| recipient_detail_address | null | `null` | 수령인 상세 주소 (배송지에서 플래튼, 미입력 시 null) | +| delivery_memo | null | `null` | 배송 메모 (배송지에서 플래튼, 미입력 시 null) | +| delivery_memo_label | null | `null` | `delivery_memo` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| options | array | `[{"id":612,"option_status":"payment_complete","option_sta…` | 주문 옵션(품목) 목록 (OrderOptionResource — 상품·옵션·수량·옵션상태·금액) | +| shipping_address | object | `{"id":320,"address_type":"shipping","orderer_name":"설창용",…` | 배송지 상세 (OrderAddressResource — 주문자/수령인/국내·해외 주소) | +| billing_address | null | `null` | 청구지 상세 (OrderAddressResource, 미분리 시 null) | +| payment | object | `{"id":284,"payment_status":"paid","payment_status_label":…` | 대표 결제 정보 (OrderPaymentResource — 결제수단·결제상태·금액) | +| payments | array | `[{"id":284,"payment_status":"paid","payment_status_label"…` | 결제 이력 목록 (OrderPaymentResource 배열 — 다회 결제/부분결제 포함) | +| shippings | array | `[]` | 배송 이력 목록 (OrderShippingResource 배열 — 배송유형·택배사·송장번호) | +| cancels | array | `[]` | 취소 이력 목록 (OrderCancelResource 배열 — 취소 사유·상세·취소일시, 최근순) | +| promotions_applied_snapshot | object | `{"coupon_issue_ids":[7330],"item_coupons":[],"discount_co…` | 적용된 프로모션 스냅샷 (재계산용) | +| shipping_policy_applied_snapshot | null | `null` | 적용된 배송정책 스냅샷 (재계산용) | +| admin_memo | null | `null` | 관리자 메모 (내부 관리용) | +| customer_memo | null | `null` | 고객 메모 (주문 시 고객이 남긴 메모) | +| created_at | string | `2026-06-17T02:07:25+00:00` | 생성 일시 | +| updated_at | string | `2026-06-17T02:07:25+00:00` | 최종 수정 일시 | +| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_update":true,"can_cancel":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 특정 주문(`order`)의 전체 상세를 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.orders.read` 권한이 필요하며, `Admin\OrderController@show`가 `OrderService::getDetail()`로 옵션·배송·결제·취소 이력·금액 내역(과세/면세/다중통화 포함)까지 풀로드해 `OrderResource`로 반환합니다. 관리자 주문 상세 화면의 데이터 소스이며, 주문이 없으면 404를 반환합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/orders/{order} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.orders.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\OrderController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | path | string | 예 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| order_status | body | string | 아니오 | — | 변경할 주문상태 (OrderStatusEnum 값, 현재 상태에서 전이 가능한 값만 허용) | +| admin_memo | body | string | 아니오 | max 2000 | 관리자 메모 (내부 관리용, 고객 비노출) | +| recipient_name | body | string | 예 | max 50 | 수령인 이름 | +| recipient_phone | body | string | 아니오 | max 20 | 수령인 연락처 | +| recipient_tel | body | string | 아니오 | max 20 | 수령인 일반전화 (recipient_phone 없을 때 필수) | +| recipient_zipcode | body | string | 아니오 | max 10 | 수령인 우편번호 (국내 주소, 해외 주소 없을 때 필수) | +| recipient_address | body | string | 아니오 | max 255 | 수령인 기본 주소 (국내 주소, 해외 주소 없을 때 필수) | +| recipient_detail_address | body | string | 아니오 | max 255 | 수령인 상세 주소 (recipient_address 입력 시 필수) | +| address_line_1 | body | string | 아니오 | max 255 | 주소 1행 (기본 주소) | +| address_line_2 | body | string | 아니오 | max 255 | 주소 2행 (상세 주소) | +| intl_city | body | string | 아니오 | max 100 | 도시 (국제 주소) | +| intl_state | body | string | 아니오 | max 100 | 주/도 (국제 주소) | +| intl_postal_code | body | string | 아니오 | max 20 | 우편번호 (국제 주소) | +| delivery_memo | body | string | 아니오 | max 500 | 배송 메모 (배송 시 요청사항) | +| recipient_country_code | body | string | 아니오 | — | 수령인 배송국가 코드 (ISO 3166-1 alpha-2 2자리) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 특정 주문(`order`)의 주문상태·관리자 메모·수취인 배송지(국내/해외 주소 포함)를 수정합니다. `auth:sanctum` + `sirsoft-ecommerce.orders.update` 권한이 필요하며, `Admin\OrderController@update`가 `OrderService::update()`로 처리한 뒤 수정된 주문을 `OrderResource`로 반환합니다. 관리자 주문 상세에서 배송지 정정·메모 기록·상태 변경 등에 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/orders/{order}/cancel + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.orders.cancel` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\OrderController@cancelOrder` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | path | string | 예 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| type | body | string | 예 | `full`, `partial` | 취소 유형 (full 전체취소 / partial 부분취소 — partial 이면 items 필수) | +| reason | body | string | 예 | — | 취소 사유 코드 (ClaimReason 의 refund·활성 코드) | +| reason_detail | body | string | 아니오 | max 500 | 취소 사유 상세 (관리자 입력 자유 텍스트) | +| items | body | array | 아니오 | min 1 | 처리 대상 항목 배열 | +| cancel_pg | body | boolean | 아니오 | — | PG 결제 취소 동반 여부 (미지정 시 기본 true — 실제 PG 취소 수행) | +| refund_priority | body | string | 아니오 | `pg_first`, `points_first` | 환불 배분 우선순위 (pg_first PG 우선 / points_first 포인트 우선, 기본 pg_first) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 특정 주문(`order`)을 전체취소 또는 부분취소합니다. `auth:sanctum` + `sirsoft-ecommerce.orders.update` 권한이 필요하며, `Admin\OrderController@cancelOrder`가 `items` 유무에 따라 `OrderCancellationService`의 `cancelOrder()`(전체) 또는 `cancelOrderOptions()`(부분)를 호출합니다. 취소자(`cancelledBy`)로 관리자 ID가 기록되고, `cancel_pg`로 PG 결제 취소 동반 여부를, `refund_priority`로 PG/포인트 환불 우선순위를 지정합니다. 취소 후 갱신된 주문을 `OrderResource`로 반환하며, 취소 불가 상태 등 실패 시 422를 반환합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/orders/{order}/confirm-deposit + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.orders.confirm-deposit` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\OrderController@confirmDeposit` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | path | string | 예 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| amount | body | number | 예 | min 0 | 확인된 입금액 (결제예정금액과 정확히 일치해야 함, 불일치 시 422) | +| depositor_name | body | string | 아니오 | max 100 | depositor 이름 (식별자) | +| mark_order_complete | body | boolean | 아니오 | — | 입금확인과 동시에 주문완료 처리 여부 (미지정 시 기본 false — 결제완료 전이만) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 무통장(dbank) 미결제 주문(`order`)의 입금을 확인해 결제완료로 전이합니다. `auth:sanctum` + `sirsoft-ecommerce.orders.update` 권한이 필요하며, `Admin\OrderController@confirmDeposit`가 `OrderProcessingService::confirmManualDeposit()`으로 입금자명·입금액을 기록하고 결제완료 처리합니다. 입금액(`amount`)이 결제예정금액과 정확히 일치하지 않으면 422(deposit_amount_mismatch)를 반환하며, `mark_order_complete`로 결제완료와 동시에 주문완료 처리 여부를 지정할 수 있습니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/orders/{order}/estimate-refund + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.orders.estimate-refund` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\OrderController@estimateRefund` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | path | string | 예 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| items | body | array | 예 | min 1 | 처리 대상 항목 배열 | +| refund_priority | body | string | 아니오 | `pg_first`, `points_first` | 환불 배분 우선순위 (pg_first PG 우선 / points_first 포인트 우선, 기본 pg_first) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 특정 주문(`order`)의 선택 옵션(`items`) 취소 시 예상 환불 금액을 실제 취소 없이 미리 계산합니다. `auth:sanctum` + `sirsoft-ecommerce.orders.update` 권한이 필요하며, `Admin\OrderController@estimateRefund`가 `OrderCancellationService::previewRefund()`로 환불 예상값을 반환합니다. `refund_priority`에 따라 PG 우선/포인트 우선 환불 배분 결과가 달라집니다. 취소 화면에서 "환불 예정 금액"을 관리자에게 미리 보여주는 용도입니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/orders/{order}/logs + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.orders.logs` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\OrderController@logs` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | path | string | 예 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| sort_order | query | string | 아니오 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 특정 주문(`order`)의 활동 로그(주문·주문옵션·배송지 변경 이력 합산)를 페이지네이션 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.orders.read` 권한이 필요하며, `Admin\OrderController@logs`가 `OrderService::getActivityLogs()`로 조회해 `ActivityLogResource`로 반환합니다. `sort_order`로 시간 정렬 방향을 지정할 수 있습니다. 관리자 주문 상세의 "처리 이력" 탭에서 누가 언제 무엇을 변경했는지 추적하는 용도입니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/orders/{order}/options/bulk-status + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.orders.options.bulk-status` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\OrderController@bulkChangeOptionStatus` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | path | string | 예 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| items | body | array | 예 | min 1 | 처리 대상 항목 배열 | +| status | body | string | 예 | — | 일괄 전환할 옵션 상태 (OrderStatusEnum 값, 옵션별 전이 규칙 검증) | +| carrier_id | body | integer | 아니오 | — | carrier 식별자 | +| tracking_number | body | string | 아니오 | max 50 | 송장(운송장)번호 (배송 관련 상태로 전환 시 carrier_id 와 함께 필수) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 특정 주문(`order`)의 여러 주문 옵션 상태를 수량 분할까지 지원해 일괄 변경합니다. `auth:sanctum` + `sirsoft-ecommerce.orders.update` 권한이 필요하며, `Admin\OrderController@bulkChangeOptionStatus`가 `status`를 `OrderStatusEnum`으로 변환한 뒤 `OrderOptionService::bulkChangeStatusWithQuantity()`로 처리합니다. 배송중으로 전환 시 `carrier_id`·`tracking_number`(택배사·송장번호)를 함께 넘길 수 있습니다. 한 옵션의 일부 수량만 상태 전환(부분 배송 등)하는 시나리오를 지원합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/orders/{order}/reset-guest-lookup-password + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.orders.reset-guest-lookup-password` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\OrderController@resetGuestLookupPassword` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | path | string | 예 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| guest_lookup_password | body | string | 예 | min 8, max 255 | 재설정할 비회원 주문 조회 비밀번호 (8자 이상, 해시로 저장 · 회원가입 정책과 동일) | +| guest_lookup_password_confirmation | body | string | 예 | — | 조회 비밀번호 확인 (guest_lookup_password 와 일치해야 함) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 비회원 주문(`order`)의 조회 비밀번호를 재설정합니다. `auth:sanctum` + `sirsoft-ecommerce.orders.update` 권한이 필요하며, 비회원 주문(`user_id IS NULL`)만 허용하고 회원 주문에는 422를 반환합니다. `Admin\OrderController@resetGuestLookupPassword`가 `OrderService::resetGuestLookupPassword()`로 새 비밀번호를 해시로 저장하며, 평문은 응답/로그에 노출하지 않습니다. 비회원이 조회 비밀번호를 분실했을 때 관리자가 대신 재설정해 주는 용도입니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/orders/{order}/send-email + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.orders.send-email` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\OrderController@sendEmail` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | path | string | 예 | — | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| email | body | email | 예 | max 255 | 이메일 주소 | +| message | body | string | 예 | max 5000 | 관리자가 작성한 안내 메일 본문 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 특정 주문(`order`)에 대해 주문 관련 안내 이메일을 지정 주소(`email`)로 발송합니다. `auth:sanctum` + `sirsoft-ecommerce.orders.update` 권한이 필요하며, `Admin\OrderController@sendEmail`이 `OrderService::sendEmail()`로 관리자가 작성한 메시지(`message`)를 전송합니다. 주문 관련 개별 안내가 필요할 때 관리자가 상세 화면에서 수동으로 메일을 보내는 용도입니다. + + +### POST /api/modules/sirsoft-ecommerce/orders/{orderNumber}/cancel-payment + +- **라우트명**: `api.modules.sirsoft-ecommerce.orders.cancel-payment` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\OrderController@cancelPayment` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +| cancel_code | body | string | 아니오 | max 100 | PG사 취소 코드 (예: USER_CANCEL, order_payments 취소 이력에 기록) | +| cancel_message | body | string | 아니오 | max 500 | PG사 취소 메시지 (order_payments 취소 이력에 기록) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 회원/비회원이 PG 결제창을 닫았을 때 결제 취소 이력만 기록합니다. `optional.sanctum`으로 회원/비회원 모두 접근하며, `Public\OrderController@cancelPayment`가 `OrderProcessingService::recordPaymentCancellation()`으로 주문 상태는 변경하지 않고 `order_payments`에 취소창 닫힘 이력(`cancel_code`·`cancel_message`)만 남깁니다. 결제 SDK가 사용자 취소 콜백을 받았을 때 프론트가 호출해 결제 시도 이력을 추적하는 용도입니다. + + +### GET /api/modules/sirsoft-ecommerce/user/orders + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.orders.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\OrderController@index` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| per_page | query | integer | 아니오 | min 1, max 50 | 페이지당 항목 수 | +| status | query | string | 아니오 | — | 상태 필터 (해당 상태의 항목만 조회) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `452` | 기본 키 (내부 식별자) | +| order_number | string | `20260625-1144420949` | 주문번호 (사용자 노출용 고유 식별 코드) | +| status | string | `shipping` | 주문상태 값 (OrderStatusEnum value — 마이페이지용 status 별칭) | +| status_label | string | `배송중` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| status_variant | string | `primary` | 상태 표시 색상/스타일 변형 키 (상태 Enum variant() 산물 — UI 배지용) | +| is_partially_cancelled | boolean | `false` | partially cancelled 여부 | +| recipient_country_code | string | `KR` | 배송국가 코드 (ISO 3166-1 alpha-2, shippingAddress 로드 시) | +| recipient_country_name | object | `{"ko":"한국","en":"South Korea"}` | 배송국가 현지화명 (로케일별 국가명 맵) | +| ordered_at | string | `2026-06-25T11:44:42+00:00` | ordered 일시 | +| ordered_at_formatted | string | `2026-06-25 20:44:42` | `ordered_at` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_amount | integer | `31000` | 최종 주문금액 (상품합계 − 할인 + 배송비) | +| total_amount_formatted | string | `31,000원` | `total_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| mc_total_amount | object | `{"KRW":{"amount":31000,"formatted":"31,000원"},"USD":{"amo…` | 최종 주문금액 다중 통화 (주문 시점 스냅샷, 통화별 amount·formatted) | +| total_shipping_amount | integer | `0` | 총 배송비 | +| total_shipping_amount_formatted | string | `0원` | `total_shipping_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| mc_total_shipping_amount | object | `{"KRW":{"amount":0,"formatted":"0원"},"USD":{"amount":0,"f…` | 총 배송비 다중 통화 (주문 시점 스냅샷, 통화별 amount·formatted) | +| total_points_used_amount | integer | `0` | 총 포인트(마일리지) 사용액 | +| total_points_used_amount_formatted | string | `0원` | `total_points_used_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| total_earned_points_amount | integer | `310` | 총 적립 예정 포인트 | +| total_earned_points_amount_formatted | string | `310원` | `total_earned_points_amount` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| items | array | `[{"product_name":"프리미엄 샤인머스캣 2kg #94","product_option_nam…` | 주문 품목 목록 (상품명·옵션명·썸네일·수량·단가/소계·추가옵션 요약) | +| item_count | integer | `1` | item 개수 (집계) | +| abilities | object | `{"can_view":true,"can_cancel":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 회원이 마이페이지 주문내역에서 본인 주문 목록을 상태별 통계와 함께 페이지네이션 조회합니다. `auth:sanctum` 인증이 필요하며, `User\OrderController@index`가 `user_id`를 본인으로 고정한 뒤 `OrderService::getList()`와 `getUserStatistics()`를 호출해 `UserOrderCollection`으로 반환합니다. `status`로 특정 주문상태만 필터링할 수 있습니다. 관리자 목록과 달리 항상 본인 주문으로만 한정됩니다. + + +### POST /api/modules/sirsoft-ecommerce/user/orders + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.orders.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\OrderController@store` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-ecommerce.user-orders.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| payment_method | body | string | 예 | — | 결제수단 (PaymentMethodEnum 값 — card/vbank/dbank 등) | +| expected_total_amount | body | number | 예 | min 0 | 프론트가 계산한 예상 결제금액 (서버 재계산값과 대조해 금액 위변조 검증) | +| shipping_memo | body | string | 아니오 | max 500 | 배송 요청사항 메모 | +| depositor_name | body | string | 아니오 | max 50 | depositor 이름 (식별자) | +| save_shipping_address | body | boolean | 아니오 | — | 회원 주소록에 이번 배송지 저장 여부 (회원 주문 한정) | +| guest_lookup_password | body | string | 예 | min 8, max 255 | 비회원 주문 조회 비밀번호 (비회원만 필수, 8자 이상 · 해시로 저장) | +| guest_lookup_password_confirmation | body | string | 예 | — | 조회 비밀번호 확인 (guest_lookup_password 와 일치해야 함) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.order.create_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-orders.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 주문서 작성을 마치고 실제 주문을 생성(결제하기)하는 회원/비회원 공용 엔드포인트입니다. PG 플러그인의 fetch 인터셉터가 이 한 경로만 매칭하므로 회원/비회원이 동일 URL로 진입하고, `Public\OrderController@store`가 `Auth::id()`로 분기합니다(회원은 `OrderResource`, 비회원은 민감 필드를 가린 `GuestOrderResource`). `optional.sanctum` + `sirsoft-ecommerce.user-orders.create` 권한이 필요하며, `expected_total_amount`로 금액 위변조를 검증하고 비회원은 `guest_lookup_password`로 이후 조회 비밀번호를 설정합니다. 회원이 `save_shipping_address`를 켜면 배송지가 자동 저장(PG 결제는 결제완료 시점) 됩니다. + + +### GET /api/modules/sirsoft-ecommerce/user/orders/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.orders.show-by-id` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\OrderController@show` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 회원이 마이페이지 주문 상세에서 주문 ID(`id`)로 본인 주문의 전체 상세를 조회합니다. `auth:sanctum` 인증이 필요하며, `User\OrderController@show`가 `OrderService::getDetail()`로 로드한 뒤 소유자 검증(`user_id === Auth::id()`)을 거쳐 `OrderResource`로 반환합니다. 본인 주문이 아니거나 존재하지 않으면 정보 노출 방지를 위해 404를 반환합니다. 주문번호로 조회하는 `showByOrderNumber`와 달리 내부 주문 ID를 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/user/orders/{id}/cancel + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.orders.cancel` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\OrderController@cancel` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.user-orders.cancel` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| reason | body | string | 예 | — | 취소 사유 코드 (ClaimReason 의 refund·활성·사용자 선택 가능 코드) | +| reason_detail | body | string | 아니오 | max 500 | 취소 사유 상세 (회원 입력 자유 텍스트) | +| items | body | array | 아니오 | min 1 | 처리 대상 항목 배열 | +| refund_priority | body | string | 아니오 | `pg_first`, `points_first` | 환불 배분 우선순위 (pg_first PG 우선 / points_first 포인트 우선, 기본 pg_first) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-orders.cancel`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 회원이 마이페이지에서 본인 주문(`id`)을 취소합니다. `auth:sanctum` + `sirsoft-ecommerce.user-orders.cancel` 권한이 필요하며, `User\OrderController@cancel`이 `items` 유무에 따라 `OrderCancellationService`의 `cancelOrderOptions()`(부분) 또는 `cancelOrder()`(전체)를 호출합니다. 취소자(`cancelledBy`)로 회원 본인 ID가 기록되고, `refund_priority`로 PG/포인트 환불 우선순위를 지정합니다. 취소 가능 상태의 주문만 취소되며, 취소 후 갱신된 주문을 `OrderResource`로 반환합니다. + + +### POST /api/modules/sirsoft-ecommerce/user/orders/{id}/estimate-refund + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.orders.estimate-refund` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\OrderController@estimateRefund` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.user-orders.cancel` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| items | body | array | 예 | min 1 | 처리 대상 항목 배열 | +| refund_priority | body | string | 아니오 | `pg_first`, `points_first` | 환불 배분 우선순위 (pg_first PG 우선 / points_first 포인트 우선, 기본 pg_first) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-orders.cancel`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 회원이 마이페이지에서 본인 주문(`id`)의 선택 옵션(`items`) 취소 시 예상 환불 금액을 실제 취소 없이 미리 계산합니다. `auth:sanctum` + `sirsoft-ecommerce.user-orders.cancel` 권한이 필요하며, `User\OrderController@estimateRefund`가 `OrderCancellationService::previewRefund()`로 환불 예상값을 반환합니다. `refund_priority`에 따라 PG 우선/포인트 우선 환불 배분 결과가 달라집니다. 취소 확정 전 "환불 예정 금액"을 회원에게 안내하는 용도입니다. + + +### POST /api/modules/sirsoft-ecommerce/user/orders/{id}/options/{optionId}/confirm + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.orders.confirm-option` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\OrderController@confirmOption` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.user-orders.confirm` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| optionId | path | string | 예 | — | 대상 option의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-orders.confirm`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 회원이 마이페이지에서 본인 주문(`id`)의 개별 옵션(`optionId`)을 구매확정합니다. `auth:sanctum` + `sirsoft-ecommerce.user-orders.confirm` 권한이 필요하며, `User\OrderController@confirmOption`이 `OrderService::confirmOption()`을 호출합니다. 구매확정 시 적립 포인트 확정 등 후속 처리가 이어지며, 확정 불가 상태(배송 미완료 등)면 422를 반환합니다. 배송 완료된 상품을 회원이 직접 "구매확정" 할 때 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/user/orders/{id}/reorder + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.orders.reorder` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\OrderController@reorder` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 회원이 과거 주문(`id`)의 옵션들을 현재 장바구니에 다시 담는 재주문 기능입니다. `auth:sanctum` 인증이 필요하며, `User\OrderController@reorder`가 `CartService::reorderFromOrder()`로 처리해 담긴 수량(`added_count`), 담지 못한 항목(`skipped[]`), 현재 장바구니 총 개수(`cart_count`)를 반환합니다. 취소된 주문도 재주문 대상이 되며, 품절·단종 등으로 추가 불가한 항목은 건너뛰어 `skipped` 배열로 안내합니다. 마이페이지 주문내역의 "재주문" 버튼에 사용합니다. + + +### PUT /api/modules/sirsoft-ecommerce/user/orders/{id}/shipping-address + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.orders.update-shipping-address` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\OrderController@updateShippingAddress` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| address_id | body | integer | 아니오 | — | address 식별자 | +| recipient_name | body | string | 아니오 | max 50 | 수령인 이름 | +| recipient_phone | body | string | 아니오 | max 20 | 수령인 연락처 | +| country_code | body | string | 아니오 | — | 국가 코드 (ISO 3166-1 alpha-2) | +| zipcode | body | string | 아니오 | max 10 | 우편번호 | +| address | body | string | 아니오 | max 255 | 기본 주소 | +| address_detail | body | string | 아니오 | max 255 | 상세 주소 | +| address_line_1 | body | string | 아니오 | max 255 | 주소 1행 (기본 주소) | +| address_line_2 | body | string | 아니오 | max 255 | 주소 2행 (상세 주소) | +| intl_city | body | string | 아니오 | max 100 | 도시 (국제 주소) | +| intl_state | body | string | 아니오 | max 100 | 주/도 (국제 주소) | +| intl_postal_code | body | string | 아니오 | max 20 | 우편번호 (국제 주소) | +| delivery_memo | body | string | 아니오 | max 255 | 배송 메모 (배송 시 요청사항) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.order.shipping_address_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 회원이 배송 전 상태의 본인 주문(`id`) 배송지를 변경합니다. `auth:sanctum` 인증이 필요하며, `User\OrderController@updateShippingAddress`가 소유자 검증 후 `OrderService::updateShippingAddress()`로 처리합니다. 저장된 회원 주소(`address_id`)를 선택하거나 수취인·연락처·주소 필드를 직접 입력할 수 있고, 국내(`zipcode`/`address`)와 해외(`address_line_1`·`intl_city` 등) 주소를 모두 지원합니다. 이미 배송이 시작된 주문 등 변경 불가 상태면 422를 반환합니다. + + +### GET /api/modules/sirsoft-ecommerce/user/orders/{orderNumber} + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.orders.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\OrderController@showByOrderNumber` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 주문번호(`orderNumber`)로 주문 상세를 조회하는 회원/비회원 공용 엔드포인트입니다. `optional.sanctum`으로 로그인 여부에 따라 분기하는데, 로그인 상태면 본인 회원 주문만 `OrderResource`로 반환하고 아니면 404(마이페이지 주문 목록으로 안내), 비로그인이면 `X-Guest-Order-Token`으로 비회원 주문을 매칭해 `GuestOrderResource`로 반환하고 실패 시 404(비회원 조회 폼으로 안내)합니다. 회원이 비회원 토큰을 들고 와도 회원 분기가 우선하며, 실패 사유는 모두 동일한 404로 처리해 정보 노출을 차단합니다. 결제 완료 후 주문번호 기반 주문 완료/상세 페이지에서 사용합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/payments.md b/modules/_bundled/sirsoft-ecommerce/docs/api/payments.md new file mode 100644 index 00000000..17f1dde5 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/payments.md @@ -0,0 +1,46 @@ +# Payments API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Payments 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/payments/client-config/{provider} + +- **라우트명**: `api.modules.sirsoft-ecommerce.payments.client-config` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Shop\PaymentConfigController@clientConfig` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| provider | path | string | 예 | — | 대상 provider의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** PG 제공자(`provider`, 예: `tosspayments`)의 프론트엔드 결제 SDK 초기화에 필요한 클라이언트 설정을 반환하는 공개 엔드포인트입니다. 인증이 필요 없으며, 결제 페이지가 결제창을 띄우기 직전에 호출합니다. 실제 설정값은 `PaymentConfigController@clientConfig`가 `sirsoft-ecommerce.payment.get_client_config` 필터 훅을 실행해 각 PG 플러그인이 등록한 `client_key`·`sdk_url` 등을 수집한 결과이며, 코어는 어떤 PG도 하드코딩하지 않습니다. 해당 provider에 등록된 설정이 없으면(플러그인 미설치·미활성) 404를 반환합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/presets.md b/modules/_bundled/sirsoft-ecommerce/docs/api/presets.md new file mode 100644 index 00000000..35d8d1b8 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/presets.md @@ -0,0 +1,145 @@ +# Presets API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Presets 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/presets + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.presets.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\SearchPresetController@index` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| target_screen | query | string | 아니오 | `products`, `orders`, `coupons` | 대상 검색 화면 (해당 화면의 프리셋만 조회, 미지정 시 `products`) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.search_preset.list_validation_rules`). + +**응답 필드** (`data` 내부) + + + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자 검색 화면(상품/주문/쿠폰 등)에서 저장해 둔 검색 조건 프리셋 목록을 조회합니다. `auth:sanctum` 인증이 필요하며, `SearchPresetService::getPresets()`가 `target_screen`에 해당하는 프리셋을 반환합니다(미지정 시 `products` 화면 기준). 관리자가 자주 쓰는 필터 조합을 프리셋으로 관리해 검색 화면에서 빠르게 적용하기 위한 용도이며, 확장이 `list_validation_rules` 훅으로 조회 파라미터를 추가할 수 있습니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/presets + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.presets.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\SearchPresetController@store` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| target_screen | body | string | 아니오 | — | 프리셋이 속할 검색 화면 (`products`/`orders`/`coupons`, 미지정 시 `products`) | +| name | body | string | 예 | max 100 | 대상의 이름/명칭 | +| conditions | body | array | 예 | — | 저장할 검색 조건 배열 (필터 필드/값 조합, 적용 시 그대로 복원됨) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.preset.store_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자 검색 화면의 새 검색 조건 프리셋을 생성합니다. `auth:sanctum` 인증이 필요하며, `SearchPresetService::create()`가 `target_screen`(미지정 시 `products`)·`name`(최대 100자)·`conditions`(검색 조건 배열)을 받아 프리셋을 저장하고 성공 시 `201`로 생성된 프리셋을 반환합니다. 자주 쓰는 필터 조합을 이름 붙여 저장해 재사용하기 위한 용도이며, 확장이 `preset.store_validation_rules` 훅으로 파라미터를 추가할 수 있습니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/presets/{preset} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.presets.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\SearchPresetController@destroy` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| preset | path | string | 예 | — | 대상 preset의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 지정한 검색 조건 프리셋 1건을 삭제합니다. `auth:sanctum` 인증이 필요하며, path의 `{preset}`은 라우트 모델 바인딩으로 `SearchPreset` 모델이 주입되고 `SearchPresetService::delete()`가 삭제를 수행합니다. 삭제 성공 시 `{ "deleted": true }`를 반환하며, 존재하지 않는 프리셋이면 `404`를 반환합니다. 더 이상 사용하지 않는 저장 검색 조합을 정리하는 용도입니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/presets/{preset} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.presets.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\SearchPresetController@update` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| preset | path | string | 예 | — | 대상 preset의 식별자 | +| name | body | string | 예 | max 100 | 대상의 이름/명칭 | +| conditions | body | array | 예 | — | 저장할 검색 조건 배열 (필터 필드/값 조합, 적용 시 그대로 복원됨) | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.preset.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 기존 검색 조건 프리셋 1건을 수정합니다. `auth:sanctum` 인증이 필요하며, path의 `{preset}`은 라우트 모델 바인딩으로 주입되고 `SearchPresetService::update()`가 `name`(최대 100자)·`conditions`(검색 조건 배열)·선택 `sort_order`(정렬 순서, 0 이상)를 반영해 저장 후 갱신된 프리셋을 반환합니다. 저장해 둔 필터 조합의 이름·조건·노출 순서를 변경하는 용도이며, 확장이 `preset.update_validation_rules` 훅으로 파라미터를 추가할 수 있습니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/product-common-infos.md b/modules/_bundled/sirsoft-ecommerce/docs/api/product-common-infos.md new file mode 100644 index 00000000..311acdde --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/product-common-infos.md @@ -0,0 +1,224 @@ +# Product Common Infos API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Product Common Infos 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/product-common-infos + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-common-infos.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductCommonInfoController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-common-infos.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `207` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"일반 배송 안내","en":"Standard Shipping"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `일반 배송 안내` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| content | object | `{"ko":"• 배송 기간: 결제 완료 후 1~3일 이내 출고 (영업일 기준)\n• 배송 업체: CJ대…` | 본문 내용 | +| localized_content | string | `• 배송 기간: 결제 완료 후 1~3일 이내 출고 (영업일 기준) …` | `content` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| content_mode | string | `text` | 내용 표시 모드 (`text` 일반 텍스트 / `html` HTML, 기본값 `text`) | +| is_default | boolean | `true` | default 여부 | +| is_active | boolean | `true` | active 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| icon | string | `info-circle` | 아이콘 식별자 (아이콘 클래스/이름) | +| created_at | string | `2026-06-15 11:24:00` | 생성 일시 | +| updated_at | string | `2026-06-15 11:24:00` | 최종 수정 일시 | +| products_count | integer | `81` | products 개수 (집계) | +| language_count | integer | `2` | language 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-common-infos.read`)이 없는 경우 | + + + +**설명** 관리자가 상품 공통정보(배송·교환·반품 안내 등 여러 상품에 재사용되는 안내문) 목록을 조회합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-common-infos.read` 권한이 필요하며, `ProductCommonInfoController@index`가 `search`·`active_only`·`default_only` 필터를 조립합니다. `per_page`가 0 이하이거나 `all`이면 `ProductCommonInfoService::getAllCommonInfos()`로 전체를 조회하고, 그 외에는 `getPaginatedCommonInfos()`로 페이지네이션 조회(`data.pagination` 포함)합니다. `localized_name`·`localized_content`는 현재 로케일로 해석된 값, `products_count`는 이 공통정보를 사용하는 상품 수입니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/product-common-infos + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-common-infos.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductCommonInfoController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-common-infos.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| content | body | array | 아니오 | — | 본문 내용 | +| content_mode | body | string | 아니오 | `text`, `html` | 내용 표시 모드 (`text` 일반 텍스트 / `html` HTML, 미지정 시 `text`) | +| is_default | body | boolean | 아니오 | — | 기본값 지정 여부 | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-common-info.create_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-common-infos.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 새 상품 공통정보를 생성합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-common-infos.create` 권한이 필요하며, `ProductCommonInfoController@store`가 `ProductCommonInfoService::createCommonInfo()`에 검증된 데이터를 전달해 저장합니다. `name`은 다국어 배열(필수), `content`는 다국어 안내 내용, `content_mode`는 `text`/`html` 중 하나이며, `is_default`·`is_active`·`sort_order`로 기본 사용 여부·활성 여부·정렬을 지정합니다. 확장이 `sirsoft-ecommerce.product-common-info.create_validation_rules` 필터로 추가 파라미터를 붙일 수 있고, 성공 시 HTTP 201을, 처리 실패 시 400 에러 응답을 반환합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/product-common-infos/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-common-infos.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductCommonInfoController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-common-infos.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-common-infos.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 상품 공통정보 1건을 삭제합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-common-infos.delete` 권한이 필요하며, `ProductCommonInfoController@destroy`가 `ProductCommonInfoService::deleteCommonInfo()`를 호출해 삭제합니다. path의 `id`에 해당하는 공통정보가 없거나 삭제 처리 중 오류가 발생하면 각각 404/400 에러 응답을 반환합니다. 여러 상품에서 참조 중인 공통정보를 삭제할 경우 노출에 영향을 줄 수 있으므로 `products_count`를 먼저 확인하는 것이 좋습니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/product-common-infos/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-common-infos.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductCommonInfoController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-common-infos.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-common-infos.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 상품 공통정보 1건의 상세를 조회합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-common-infos.read` 권한이 필요하며, `ProductCommonInfoController@show`가 `ProductCommonInfoService::getCommonInfo()`로 단건을 조회합니다. 다국어 원본(`name`, `content`)과 현재 로케일 해석값(`localized_name`, `localized_content`), `content_mode`, 기본/활성 여부를 함께 반환하며, 해당 `id`의 공통정보가 없으면 404를 반환합니다. 주로 수정 화면 진입 시 기존 값을 불러오는 데 사용됩니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/product-common-infos/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-common-infos.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductCommonInfoController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-common-infos.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| content | body | array | 아니오 | — | 본문 내용 | +| content_mode | body | string | 아니오 | `text`, `html` | 내용 표시 모드 (`text` 일반 텍스트 / `html` HTML, 미지정 시 `text`) | +| is_default | body | boolean | 아니오 | — | 기본값 지정 여부 | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-common-info.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-common-infos.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 상품 공통정보 1건을 수정합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-common-infos.update` 권한이 필요하며, `ProductCommonInfoController@update`가 `ProductCommonInfoService::updateCommonInfo()`에 검증된 데이터를 전달해 갱신합니다. `name`(다국어 배열)은 필수이고 `content`·`content_mode`·`is_default`·`is_active`·`sort_order`를 함께 변경할 수 있습니다. 확장이 `sirsoft-ecommerce.product-common-info.update_validation_rules` 필터로 파라미터를 추가할 수 있으며, 대상이 없거나 처리 실패 시 각각 404/400 에러 응답을 반환합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/product-common-infos/{id}/toggle-active + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-common-infos.toggle-active` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductCommonInfoController@toggleActive` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-common-infos.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-common-infos.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 상품 공통정보의 활성/비활성 상태를 한 번의 요청으로 토글합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-common-infos.update` 권한이 필요하며, `ProductCommonInfoController@toggleActive`가 `ProductCommonInfoService::toggleActive()`를 호출해 현재 `is_active` 값을 반전시킵니다. 반전 결과에 따라 활성화/비활성화 메시지를 구분해 응답하므로 목록 화면의 스위치 조작에 적합합니다. 대상이 없거나 처리 실패 시 각각 404/400 에러 응답을 반환합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/product-image.md b/modules/_bundled/sirsoft-ecommerce/docs/api/product-image.md new file mode 100644 index 00000000..53866d1c --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/product-image.md @@ -0,0 +1,46 @@ +# Product Image API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Product Image 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/product-image/{hash} + +- **라우트명**: `api.modules.sirsoft-ecommerce.product-image.download` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\ProductImageController@download` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 공개 API로, 해시(`hash`, 12자)로 식별되는 상품 이미지 원본 파일을 스트리밍 서빙합니다. 인증이 필요 없으며(`PublicBaseController`), `ProductImageController@download`가 먼저 `ProductImageService::findByHash()`로 이미지 레코드 존재를 확인한 뒤 `ProductImageService::download()`로 `StorageInterface::response()` 기반 `StreamedResponse`를 반환합니다. 응답에는 저장된 `mime_type`과 `Cache-Control: public, max-age=31536000`(1년) 헤더가 부여되어 브라우저/CDN 캐싱에 최적화됩니다. 해시에 해당하는 레코드가 없거나 스토리지에 실제 파일이 없으면 404 에러 응답을 반환합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/product-labels.md b/modules/_bundled/sirsoft-ecommerce/docs/api/product-labels.md new file mode 100644 index 00000000..d7f9bda7 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/product-labels.md @@ -0,0 +1,217 @@ +# Product Labels API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Product Labels 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/product-labels + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-labels.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductLabelController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-labels.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| is_active | query | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| active_only | query | boolean | 아니오 | — | 활성 라벨만 필터 (true 시 내부적으로 `is_active=true` 로 변환 — 기존 호환용) | +| search | query | string | 아니오 | max 100 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| sort | query | string | 아니오 | `name_asc`, `name_desc`, `created_asc`, `created_desc` | 정렬 기준 (필드명, `-` 접두 시 내림차순) | +| locale | query | string | 아니오 | `ko`, `en`, `fr`, `ja` | 로케일 코드 (표시 언어/지역) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `37` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 라벨","en":"API Doc Sample Label"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| color | string | `#6B7280` | 라벨 색상 코드 (`#RRGGBB` 6자리 HEX, 뱃지 배경/글자색 등 표시에 사용) | +| is_active | boolean | `true` | active 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| created_at | string | `2026-07-07 14:47:31` | 생성 일시 | +| updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | +| assignments_count | integer | `0` | assignments 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-labels.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 상품 라벨(예: "신상품", "베스트") 목록을 조회합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-labels.read` 권한이 필요하며, `ProductLabelController@index`가 `ProductLabelService::getAllLabels()`에 검증된 필터를 전달해 조회합니다. `is_active` 또는 `active_only` 로 활성 라벨만 필터링하고, `search`(라벨명 검색), `sort`(이름/생성일 정렬), `locale`(다국어 정렬 기준)을 지원합니다. `active_only=true` 는 내부적으로 `is_active=true` 로 변환되어 기존 호환성을 유지하며, 각 항목의 `assignments_count` 로 라벨이 몇 개 상품에 부여됐는지 확인할 수 있습니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/product-labels + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-labels.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductLabelController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-labels.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| color | body | string | 예 | max 20 | 라벨 색상 코드 (필수, `#RRGGBB` 6자리 HEX 형식만 허용) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-labels.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 새 상품 라벨을 생성합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-labels.create` 권한이 필요하며, `ProductLabelController@store`가 `ProductLabelService::createLabel()`에 검증된 데이터를 넘겨 저장합니다. `name`은 다국어 배열({ko, en, ...}), `color`는 라벨 색상 코드(최대 20자)이고, `is_active`·`sort_order`로 활성 여부와 정렬 순서를 지정합니다. 성공 시 HTTP 201과 함께 생성된 라벨 리소스를 반환합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/product-labels/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-labels.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductLabelController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-labels.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-labels.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 상품 라벨 1건을 삭제합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-labels.delete` 권한이 필요하며, `ProductLabelController@destroy`가 `ProductLabelService::deleteLabel()`을 호출해 삭제합니다. path의 `id`에 해당하는 라벨이 없으면 404를 반환하고, 삭제 중 오류가 발생하면 400 에러 응답을 반환합니다. 삭제 시 해당 라벨과 상품 간의 부여(assignment) 관계도 함께 정리됩니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/product-labels/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-labels.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductLabelController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-labels.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-labels.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 상품 라벨 1건의 상세 정보를 조회합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-labels.read` 권한이 필요하며, `ProductLabelController@show`가 `ProductLabelService::getLabel()`로 단건을 조회합니다. 다국어 라벨명(`name`), 색상, 활성 여부, 정렬 순서와 함께 `assignments_count`를 반환하며, 해당 `id`의 라벨이 없으면 404를 반환합니다. 주로 라벨 수정 화면 진입 시 기존 값을 불러오는 데 사용됩니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/product-labels/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-labels.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductLabelController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-labels.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| color | body | string | 예 | max 20 | 라벨 색상 코드 (필수, `#RRGGBB` 6자리 HEX 형식만 허용) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-labels.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 기존 상품 라벨 1건을 수정합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-labels.update` 권한이 필요하며, `ProductLabelController@update`가 `ProductLabelService::updateLabel()`에 검증된 데이터를 전달해 갱신합니다. `name`(다국어 배열)과 `color`는 필수이며, `is_active`·`sort_order`도 함께 변경할 수 있습니다. 대상 라벨이 없으면 404, 갱신 처리 중 오류가 발생하면 400 에러 응답을 반환합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/product-labels/{id}/toggle-status + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-labels.toggle-status` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductLabelController@toggleStatus` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-labels.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-labels.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 상품 라벨의 활성/비활성 상태를 한 번의 요청으로 토글합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-labels.update` 권한이 필요하며, `ProductLabelController@toggleStatus`가 `ProductLabelService::toggleStatus()`를 호출해 현재 `is_active` 값을 반전시킵니다. 별도의 본문 없이 path의 `id`만으로 동작하므로 목록 화면에서 스위치 조작으로 즉시 노출 여부를 바꾸는 데 적합합니다. 대상 라벨이 없으면 404, 처리 중 오류가 발생하면 400 에러 응답을 반환합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/product-notice-templates.md b/modules/_bundled/sirsoft-ecommerce/docs/api/product-notice-templates.md new file mode 100644 index 00000000..71843b2a --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/product-notice-templates.md @@ -0,0 +1,254 @@ +# Product Notice Templates API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Product Notice Templates 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/product-notice-templates + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-notice-templates.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductNoticeTemplateController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-notice-templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| search | query | string | 아니오 | max 200 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| active_only | query | boolean | 아니오 | — | true 시 활성(is_active) 템플릿만 조회 | +| per_page | query | string | 아니오 | — | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `172` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 고시템플릿","en":"API Doc Sample Notice Templ…` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `API 문서 샘플 고시템플릿` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| category | string | `clothing` | 이 템플릿이 적용되는 품목 카테고리 식별자 | +| fields | array | `[{"label":"품명","value":"샘플"}]` | 고시 항목 정의 배열 (항목별 name/content 다국어 — 상품 등록 시 고시 항목 자동 채움에 사용) | +| fields_count | integer | `1` | fields 개수 (집계) | +| is_active | boolean | `true` | active 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| icon | string | `file-alt` | 아이콘 식별자 (아이콘 클래스/이름) | +| created_at | string | `2026-07-07 14:47:31` | 생성 일시 | +| updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 상품정보제공고시 템플릿(전자상거래법상 품목별 필수 고지 항목 세트) 목록을 조회합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-notice-templates.read` 권한이 필요하며, `ProductNoticeTemplateController@index`가 `search`·`active_only` 필터를 조립합니다. `per_page`가 0 이하이거나 `all`이면 `ProductNoticeTemplateService::getAllTemplates()`로 전체를, 그 외에는 `getPaginatedTemplates()`로 페이지네이션 조회(`data.pagination` 포함)합니다. 각 항목은 다국어 템플릿명, 품목 `category`, 고시 항목 배열 `fields`(및 `fields_count`)를 포함합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/product-notice-templates + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-notice-templates.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductNoticeTemplateController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-notice-templates.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| category | body | string | 아니오 | max 100 | 이 템플릿이 적용되는 품목 카테고리 식별자 | +| fields | body | array | 예 | min 1 | 고시 항목 정의 배열 (항목별 name/content 다국어, 최소 1개) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-notice-template.create_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 새 상품정보제공고시 템플릿을 생성합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-notice-templates.create` 권한이 필요하며, `ProductNoticeTemplateController@store`가 `ProductNoticeTemplateService::createTemplate()`에 검증된 데이터를 전달해 저장합니다. `name`(다국어 배열)과 `fields`(고시 항목 정의, 최소 1개)는 필수이고, `category`(품목 카테고리), `is_active`, `sort_order`는 선택입니다. 확장이 `sirsoft-ecommerce.product-notice-template.create_validation_rules` 필터로 파라미터를 추가할 수 있으며, 성공 시 HTTP 201을, 처리 실패 시 400 에러 응답을 반환합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/product-notice-templates/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-notice-templates.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductNoticeTemplateController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-notice-templates.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 상품정보제공고시 템플릿 1건을 삭제합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-notice-templates.delete` 권한이 필요하며, `ProductNoticeTemplateController@destroy`가 `ProductNoticeTemplateService::deleteTemplate()`를 호출해 삭제합니다. path의 `id`에 해당하는 템플릿이 없거나 삭제 처리 중 오류가 발생하면 각각 404/400 에러 응답을 반환합니다. 삭제된 템플릿은 이후 신규 상품의 고시 항목 자동 채움에 더 이상 사용되지 않습니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/product-notice-templates/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-notice-templates.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductNoticeTemplateController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-notice-templates.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 상품정보제공고시 템플릿 1건의 상세를 조회합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-notice-templates.read` 권한이 필요하며, `ProductNoticeTemplateController@show`가 `ProductNoticeTemplateService::getTemplate()`로 단건을 조회합니다. 다국어 템플릿명(`name`, `localized_name`), 품목 `category`, 고시 항목 배열 `fields`, 활성 여부·정렬 순서를 반환하며, 해당 `id`의 템플릿이 없으면 404를 반환합니다. 주로 템플릿 수정 화면 진입 시 기존 고시 항목을 불러오는 데 사용됩니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/product-notice-templates/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-notice-templates.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductNoticeTemplateController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-notice-templates.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| category | body | string | 아니오 | max 100 | 이 템플릿이 적용되는 품목 카테고리 식별자 | +| fields | body | array | 예 | min 1 | 고시 항목 정의 배열 (항목별 name/content 다국어, 최소 1개) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-notice-template.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 상품정보제공고시 템플릿 1건을 수정합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-notice-templates.update` 권한이 필요하며, `ProductNoticeTemplateController@update`가 `ProductNoticeTemplateService::updateTemplate()`에 검증된 데이터를 전달해 갱신합니다. `name`(다국어 배열)과 `fields`(최소 1개)는 필수이고, `category`·`is_active`·`sort_order`도 함께 변경할 수 있습니다. 확장이 `sirsoft-ecommerce.product-notice-template.update_validation_rules` 필터로 파라미터를 추가할 수 있으며, 대상이 없거나 처리 실패 시 각각 404/400 에러 응답을 반환합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/product-notice-templates/{id}/copy + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-notice-templates.copy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductNoticeTemplateController@copy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-notice-templates.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.create`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 기존 상품정보제공고시 템플릿을 원본 삼아 복제본을 생성합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-notice-templates.create` 권한이 필요하며(생성 계열이므로 create 권한 사용), `ProductNoticeTemplateController@copy`가 `ProductNoticeTemplateService::copyTemplate()`를 호출해 path의 `id` 템플릿을 복사합니다. 별도 본문 없이 원본 `id`만으로 동작하며, 복제된 새 템플릿을 HTTP 201로 반환합니다. 유사한 고시 항목 세트를 반복 작성하지 않고 빠르게 파생 템플릿을 만들 때 사용하며, 원본이 없거나 처리 실패 시 각각 404/400 에러 응답을 반환합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/product-notice-templates/{id}/toggle-active + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.product-notice-templates.toggle-active` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductNoticeTemplateController@toggleActive` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.product-notice-templates.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 상품정보제공고시 템플릿의 활성/비활성 상태를 한 번의 요청으로 토글합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.product-notice-templates.update` 권한이 필요하며, `ProductNoticeTemplateController@toggleActive`가 `ProductNoticeTemplateService::toggleActive()`를 호출해 현재 `is_active` 값을 반전시킵니다. 반전 결과에 따라 활성화/비활성화 메시지를 구분해 응답하므로 목록 화면의 스위치 조작에 적합합니다. 대상이 없거나 처리 실패 시 각각 404/400 에러 응답을 반환합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/products.md b/modules/_bundled/sirsoft-ecommerce/docs/api/products.md new file mode 100644 index 00000000..19243c4e --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/products.md @@ -0,0 +1,1366 @@ +# Products API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Products 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/products + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| search_field | query | string | 아니오 | `all`, `name`, `product_code`, `sku`, `barcode` | 검색 대상 필드명 (검색어를 적용할 컬럼) | +| search_keyword | query | string | 아니오 | max 200 | 검색 키워드 (부분 일치) | +| category_id | query | integer | 아니오 | — | category 식별자 | +| no_category | query | boolean | 아니오 | — | 카테고리 미지정 상품만 필터 (true 시 어떤 카테고리에도 속하지 않은 상품 조회) | +| date_type | query | string | 아니오 | — | 기간 필터 기준 날짜 컬럼 (created_at 등록일 / updated_at 수정일) | +| start_date | query | date | 아니오 | — | 조회 기간 시작일 (이 날짜 이후 데이터) | +| end_date | query | date | 아니오 | — | 조회 기간 종료일 (이 날짜 이전 데이터) | +| sales_status | query | array | 아니오 | — | 판매상태 다중 필터 (on_sale/suspended/sold_out/coming_soon 값 배열, 해당 상태만 조회) | +| display_status | query | string | 아니오 | — | 전시상태 필터 (visible 전시 / hidden 숨김) | +| brand_id | query | integer | 아니오 | — | brand 식별자 | +| no_brand | query | boolean | 아니오 | — | 브랜드 미지정 상품만 필터 (true 시 브랜드가 없는 상품 조회) | +| tax_status | query | string | 아니오 | — | 과세여부 필터 (taxable 과세 / tax_free 면세) | +| price_type | query | string | 아니오 | — | 가격 범위 필터의 기준 가격 종류 (selling_price 판매가 / supply_price 공급가 / list_price 정가) | +| min_price | query | integer | 아니오 | min 0 | 가격 범위 필터 하한 (price_type 기준 이 값 이상) | +| max_price | query | integer | 아니오 | min 0 | 가격 범위 필터 상한 (price_type 기준 이 값 이하) | +| min_stock | query | integer | 아니오 | — | 재고 범위 필터 하한 (재고 수량이 이 값 이상) | +| max_stock | query | integer | 아니오 | — | 재고 범위 필터 상한 (재고 수량이 이 값 이하) | +| shipping_policy_id | query | integer | 아니오 | — | shipping policy 식별자 | +| sort_by | query | string | 아니오 | `created_at`, `updated_at`, `selling_price`, `stock_quantity`, `name` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| per_page | query | integer | 아니오 | min 10, max 100 | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.list_validation_rules`, `sirsoft-ecommerce.product.list_validation_messages`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `115` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `322` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"eum et quia","en":"tenetur id quae"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| name_localized | string | `eum et quia` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| product_code | string | `PROD-GJUX-1484` | 상품코드 (상품 고유 관리 식별자) | +| sku | string | `SKU-MRAD-9306` | 재고관리코드(SKU) | +| thumbnail_url | string | `/api/modules/sirsoft-ecommerce/produc…` | thumbnail URL | +| list_price | integer | `112594` | 정가 (기본통화 자릿수로 정규화된 값) | +| list_price_formatted | string | `112,594원` | `list_price` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| selling_price | integer | `88949` | 판매가 (기본통화 자릿수로 정규화된 값) | +| selling_price_formatted | string | `88,949원` | `selling_price` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| discount_rate | integer | `21` | 할인율(%) (정가 대비 판매가 할인 비율, (1 - 판매가/정가) × 100) | +| multi_currency_list_price | object | `{"KRW":{"price":112594,"formatted":"112,594원","is_default…` | 통화별 정가 맵 (통화코드 → {price, formatted, is_default, editable}, 설정된 모든 통화의 환산 정가) | +| multi_currency_selling_price | object | `{"KRW":{"price":88949,"formatted":"88,949원","is_default":…` | 통화별 판매가 맵 (통화코드 → {price, formatted, is_default, editable}, 설정된 모든 통화의 환산 판매가) | +| stock_quantity | integer | `22` | 재고 수량 (옵션 사용 시 옵션 재고 합계) | +| safe_stock_quantity | integer | `12` | 안전재고 수량 (이 값 미만이면 재고 부족으로 표시) | +| is_below_safe_stock | boolean | `false` | below safe stock 여부 | +| option_stock_sum | integer | `51` | 활성 옵션의 재고 합계 (is_active 옵션들의 stock_quantity 총합) | +| sales_status | string | `on_sale` | 판매상태 값 (on_sale 판매중 / suspended 판매중지 / sold_out 품절 / coming_soon 출시예정) | +| sales_status_label | string | `판매중` | `sales_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| sales_status_variant | string | `success` | `sales_status` 값의 표시 변형 키 (UI 배지 색상/스타일) | +| display_status | string | `visible` | 전시상태 값 (visible 전시 / hidden 숨김) | +| display_status_label | string | `전시` | `display_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| display_status_variant | string | `success` | `display_status` 값의 표시 변형 키 (UI 배지 색상/스타일) | +| categories | array | `[]` | 소속 카테고리 목록 (각 항목: id·현지화 이름·대표 여부. categories 관계 eager load 시에만 채워짐) | +| primary_category | string | `바지` | 대표 카테고리명 (is_primary 카테고리의 현지화 이름) | +| categories_with_path | array | `[]` | 소속 카테고리 목록 + 경로 (각 항목: id·breadcrumb path·대표 여부) | +| brand_name | string | `ASUS` | 브랜드명 (연관 브랜드의 현지화 이름) | +| shipping_policy_id | integer | `31` | shipping policy 식별자 (연관 리소스 참조) | +| shipping_policy_name | string | `국내 무료배송` | 배송정책명 (연관 배송정책의 현지화 이름) | +| min_purchase_qty | integer | `1` | 최소 구매 수량 (1회 주문 시 이 수량 이상 구매) | +| max_purchase_qty | integer | `0` | 최대 구매 수량 (0=무제한) | +| has_options | boolean | `false` | options 여부 | +| options_count | integer | `1` | options 개수 (집계) | +| options | array | `[{"id":1597,"option_code":"OPT-JLNF-2511","option_values"…` | 활성 옵션(SKU) 목록 (각 옵션의 코드·옵션값·가격·재고 등, ProductOptionResource) | +| review_count | integer | `0` | review 개수 (집계) | +| rating_avg | integer | `0` | 평균 별점 (공개 리뷰 별점 평균, 소수 1자리 반올림) | +| created_at | string | `2026-07-07 14:47:31` | 생성 일시 | +| updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | +| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자 상품 목록을 페이지네이션으로 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.products.read` 권한이 필요하며, `ProductService::getList()`가 검색어/카테고리/판매·전시상태/가격·재고 범위 등 다양한 필터를 적용해 목록을 반환하고 `getStatistics()`로 집계 통계를 함께 제공합니다. 응답은 `ProductCollection`으로 감싸져 `withStatistics()`로 통계가 병합됩니다. 확장은 `sirsoft-ecommerce.product.list_validation_rules` 훅으로 추가 필터 파라미터를 주입할 수 있습니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/products + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| product_code | body | string | 예 | max 50 | 상품코드 (상품 고유 관리 식별자, 상품 간 중복 불가) | +| sales_product_code | body | string | 아니오 | max 50 | 판매자 상품코드 (판매자가 직접 입력하는 관리용 코드) | +| sku | body | string | 아니오 | max 100 | 재고관리코드(SKU) | +| category_ids | body | array | 예 | min 1, max 5 | category 식별자 배열 | +| primary_category_id | body | integer | 아니오 | — | primary category 식별자 | +| brand_id | body | integer | 아니오 | — | brand 식별자 | +| list_price | body | integer | 예 | min 0.01 | 정가 (기본통화 기준, 소수 통화는 소수 입력 허용) | +| selling_price | body | integer | 예 | min 0.01 | 판매가 (기본통화 기준, 정가 이하여야 함) | +| stock_quantity | body | integer | 예 | min 0 | 재고 수량 (옵션 사용 시 옵션 재고 합계로 관리) | +| safe_stock_quantity | body | integer | 아니오 | min 0 | 안전재고 수량 (이 값 미만이면 재고 부족 표시) | +| sales_status | body | string | 예 | — | 판매상태 (on_sale 판매중 / suspended 판매중지 / sold_out 품절 / coming_soon 출시예정) | +| display_status | body | string | 예 | — | 전시상태 (visible 전시 / hidden 숨김) | +| tax_status | body | string | 예 | — | 과세여부 (taxable 과세 / tax_free 면세) | +| tax_rate | body | number | 아니오 | min 0, max 100 | 세율(%) (과세 상품의 부가세 계산 비율) | +| shipping_policy_id | body | integer | 아니오 | — | shipping policy 식별자 | +| common_info_id | body | integer | 아니오 | — | common info 식별자 | +| description | body | array | 아니오 | — | 설명 | +| description_mode | body | string | 아니오 | `text`, `html` | 상세 설명 편집 모드 (text 일반 텍스트 / html HTML 에디터) | +| thumbnail_hash | body | string | 아니오 | max 64 | 대표 이미지로 지정할 이미지 해시 (업로드된 이미지 중 썸네일 선택) | +| image_temp_key | body | string | 아니오 | max 64 | 임시 업로드 세션 키 (사전 업로드한 이미지를 이 상품에 연결) | +| images | body | array | 아니오 | max 20 | 상품 이미지 목록 (각 항목: id/hash/url/alt_text/is_thumbnail/sort_order) | +| meta_title | body | array | 아니오 | — | SEO 메타 제목 (검색엔진/소셜 공유 표시 제목) | +| meta_description | body | array | 아니오 | — | SEO 메타 설명 (검색엔진/소셜 공유 표시 요약) | +| meta_keywords | body | array | 아니오 | — | SEO 메타 키워드 배열 (검색엔진 색인용 키워드 목록) | +| seo_sync_title | body | boolean | 아니오 | — | SEO 제목 자동 동기화 여부 (true 시 상품명으로 메타 제목 자동 채움) | +| seo_sync_description | body | boolean | 아니오 | — | SEO 설명 자동 동기화 여부 (true 시 상품 설명으로 메타 설명 자동 채움) | +| use_main_image_for_og | body | boolean | 아니오 | — | 대표 이미지를 OG(소셜 공유) 이미지로 사용할지 여부 | +| has_options | body | boolean | 아니오 | — | options 여부 | +| option_groups | body | array | 아니오 | — | 옵션 그룹 정의 (예: 색상/사이즈 등 옵션 축과 각 축의 선택값 목록) | +| options | body | array | 예 | min 1 | 옵션(SKU) 목록 (각 항목: 옵션코드·옵션명·옵션값·정가·판매가·재고 등, 최소 1건 필수) | +| additional_options | body | array | 아니오 | max 5 | 추가옵션 그룹 배열 (각 그룹당 선택지 1~20개, 필수 여부·추가금·직접입력 허용 등 설정) | +| notice_items | body | array | 아니오 | max 50 | 상품정보제공고시 항목 배열 (각 항목: 항목명·내용 다국어) | +| label_assignments | body | array | 아니오 | — | 라벨 할당 배열 (label_id + 노출 시작/종료일로 상품에 라벨 부착) | +| min_purchase_qty | body | integer | 아니오 | min 1 | 최소 구매 수량 (1회 주문 시 이 수량 이상 구매) | +| max_purchase_qty | body | integer | 아니오 | min 0 | 최대 구매 수량 (0=무제한) | +| purchase_restriction | body | string | 아니오 | `none`, `restricted` | 구매 대상 제한 (none 제한 없음 / restricted 특정 역할만 구매 허용) | +| allowed_roles | body | array | 아니오 | — | 구매 허용 역할 ID 배열 (purchase_restriction=restricted 시 필수) | +| barcode | body | string | 아니오 | max 50 | 바코드 | +| hs_code | body | string | 아니오 | max 20 | HS 코드 (수출입 관세 분류 코드) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.store_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 새 상품을 생성합니다. `auth:sanctum` + `sirsoft-ecommerce.products.create` 권한이 필요하며, `StoreProductRequest`로 검증된 데이터를 `ProductService::create()`에 넘겨 상품·옵션·카테고리·이미지·SEO 메타를 함께 저장하고 성공 시 201과 `ProductResource`를 반환합니다. 이미지는 사전에 `POST .../products/images`로 임시 업로드한 뒤 `image_temp_key`(또는 `images`/`thumbnail_hash`)로 연결하며, `options`는 최소 1건 필수입니다. 검증 실패는 422, 그 외 오류는 500으로 응답합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/products/bulk-price + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.bulk-price` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@bulkUpdatePrice` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| method | body | string | 예 | `increase`, `decrease`, `set` | 변경 방식 (increase 증가 / decrease 감소 / set 지정값으로 설정) | +| value | body | number | 예 | min 0 | 값 | +| unit | body | string | 예 | `won`, `percent` | 변경 단위 (won 금액 단위 / percent 판매가 대비 비율) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.bulk_price_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 선택한 여러 상품의 판매가를 일괄 변경합니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, `ProductService::bulkUpdatePrice()`가 `ids` 목록에 대해 `method`(increase/decrease/set)와 `unit`(won/percent) 조합으로 가격을 재계산해 저장합니다. 응답의 `updated_count`가 실제 반영 건수로 메타에 담기며, 대량 변경은 상품별 개별 활동 로그로 기록됩니다. 확장은 `sirsoft-ecommerce.product.bulk_price_validation_rules` 훅으로 검증 규칙을 확장할 수 있습니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/products/bulk-status + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.bulk-status` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@bulkUpdateStatus` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| field | body | string | 예 | `sales_status`, `display_status` | 일괄 변경할 상태 필드 (sales_status 판매상태 / display_status 전시상태) | +| value | body | string | 예 | — | 값 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.bulk_status_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 선택한 여러 상품의 상태를 일괄 변경합니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, `ProductService::bulkUpdateStatus()`가 `field`(sales_status 또는 display_status)를 지정한 `value`로 `ids` 대상에 일괄 적용합니다. 예를 들어 판매중지된 상품을 한 번에 판매중으로 전환하거나 노출/숨김을 일괄 조정할 때 사용하며, 반영 건수는 `updated_count`로 반환됩니다. 확장은 `sirsoft-ecommerce.product.bulk_status_validation_rules` 훅으로 검증 규칙을 확장할 수 있습니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/products/bulk-stock + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.bulk-stock` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@bulkUpdateStock` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| method | body | string | 예 | `increase`, `decrease`, `set` | 변경 방식 (increase 증가 / decrease 감소 / set 지정값으로 설정) | +| value | body | integer | 예 | min 0 | 값 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.bulk_stock_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 선택한 여러 상품의 재고를 일괄 변경합니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, `ProductService::bulkUpdateStock()`가 `ids` 목록에 대해 `method`(increase/decrease/set)와 정수 `value`를 적용해 재고 수량을 조정합니다. 입고/재고 실사 반영 등 다건 재고 보정에 사용하며, 반영 건수는 `updated_count`로 반환됩니다. 확장은 `sirsoft-ecommerce.product.bulk_stock_validation_rules` 훅으로 검증 규칙을 확장할 수 있습니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/products/bulk-update + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.bulk-update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@bulkUpdate` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| bulk_changes | body | array | 아니오 | — | 상품 조건 기반 일괄 변경값 (지정 시 ids 전체에 sales_status/display_status 일괄 적용) | +| items | body | array | 아니오 | — | 처리 대상 항목 배열 | +| option_bulk_changes | body | array | 아니오 | — | 옵션 조건 기반 일괄 변경값 (price_adjustment/stock_quantity 를 method+value 로 일괄 조정) | +| option_items | body | array | 아니오 | — | 옵션 개별 인라인 수정 배열 (각 항목: product_id·option_id + 수정할 옵션 필드) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.bulk_update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 상품과 옵션을 통합 일괄 수정합니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, `ProductService::bulkUpdate()`가 조건 기반 일괄 변경(`bulk_changes`/`option_bulk_changes`)과 행별 인라인 수정(`items`/`option_items`)을 함께 처리합니다. 일괄 변경 조건이 지정된 필드는 우선 적용되고 나머지는 개별 수정값이 반영되며, 응답 메타의 `count`는 상품 반영 건수와 옵션 반영 건수를 합산한 값입니다. 관리자 목록 화면의 인라인 편집·일괄 편집을 한 요청으로 저장하는 데 사용됩니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/products/by-code/{code} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.show-by-code` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@showByCode` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| code | path | string | 예 | — | 대상 리소스의 코드 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 상품코드로 단일 상품의 상세 정보를 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.products.read` 권한이 필요하며, `ProductService::findByCode()`로 `code`에 해당하는 상품을 찾아 `ProductResource`로 반환합니다. ID가 아닌 판매/관리용 상품코드로 상세를 열람할 때 사용하며, 일치하는 상품이 없으면 404를 반환합니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/products/by-code/{code} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.update-by-code` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@updateByCode` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| code | path | string | 예 | — | 대상 리소스의 코드 | +| name | body | array | 아니오 | — | 대상의 이름/명칭 | +| product_code | body | string | 예 | max 50 | 상품코드 (상품 고유 관리 식별자, 상품 간 중복 불가) | +| sales_product_code | body | string | 아니오 | max 50 | 판매자 상품코드 (판매자가 직접 입력하는 관리용 코드) | +| sku | body | string | 아니오 | max 100 | 재고관리코드(SKU) | +| category_ids | body | array | 아니오 | min 1, max 5 | category 식별자 배열 | +| primary_category_id | body | integer | 아니오 | — | primary category 식별자 | +| brand_id | body | integer | 아니오 | — | brand 식별자 | +| list_price | body | integer | 아니오 | min 0.01 | 정가 (기본통화 기준, 소수 통화는 소수 입력 허용) | +| selling_price | body | integer | 아니오 | min 0.01 | 판매가 (기본통화 기준, 정가 이하여야 함) | +| stock_quantity | body | integer | 아니오 | min 0 | 재고 수량 (옵션 사용 시 옵션 재고 합계로 관리) | +| safe_stock_quantity | body | integer | 아니오 | min 0 | 안전재고 수량 (이 값 미만이면 재고 부족 표시) | +| sales_status | body | string | 아니오 | — | 판매상태 (on_sale 판매중 / suspended 판매중지 / sold_out 품절 / coming_soon 출시예정) | +| display_status | body | string | 아니오 | — | 전시상태 (visible 전시 / hidden 숨김) | +| tax_status | body | string | 아니오 | — | 과세여부 (taxable 과세 / tax_free 면세) | +| tax_rate | body | number | 아니오 | min 0, max 100 | 세율(%) (과세 상품의 부가세 계산 비율) | +| shipping_policy_id | body | integer | 아니오 | — | shipping policy 식별자 | +| common_info_id | body | integer | 아니오 | — | common info 식별자 | +| description | body | array | 아니오 | — | 설명 | +| description_mode | body | string | 아니오 | `text`, `html` | 상세 설명 편집 모드 (text 일반 텍스트 / html HTML 에디터) | +| thumbnail_hash | body | string | 아니오 | max 64 | 대표 이미지로 지정할 이미지 해시 (업로드된 이미지 중 썸네일 선택) | +| image_temp_key | body | string | 아니오 | max 64 | 임시 업로드 세션 키 (사전 업로드한 이미지를 이 상품에 연결) | +| images | body | array | 아니오 | max 20 | 상품 이미지 목록 (각 항목: id/hash/url/alt_text/is_thumbnail/sort_order) | +| meta_title | body | array | 아니오 | — | SEO 메타 제목 (검색엔진/소셜 공유 표시 제목) | +| meta_description | body | array | 아니오 | — | SEO 메타 설명 (검색엔진/소셜 공유 표시 요약) | +| meta_keywords | body | array | 아니오 | — | SEO 메타 키워드 배열 (검색엔진 색인용 키워드 목록) | +| seo_sync_title | body | boolean | 아니오 | — | SEO 제목 자동 동기화 여부 (true 시 상품명으로 메타 제목 자동 채움) | +| seo_sync_description | body | boolean | 아니오 | — | SEO 설명 자동 동기화 여부 (true 시 상품 설명으로 메타 설명 자동 채움) | +| use_main_image_for_og | body | boolean | 아니오 | — | 대표 이미지를 OG(소셜 공유) 이미지로 사용할지 여부 | +| has_options | body | boolean | 아니오 | — | options 여부 | +| option_groups | body | array | 아니오 | — | 옵션 그룹 정의 (예: 색상/사이즈 등 옵션 축과 각 축의 선택값 목록) | +| options | body | array | 아니오 | min 1 | 옵션(SKU) 목록 (각 항목: 옵션코드·옵션명·옵션값·정가·판매가·재고 등) | +| additional_options | body | array | 아니오 | max 5 | 추가옵션 그룹 배열 (각 그룹당 선택지 1~20개, 필수 여부·추가금·직접입력 허용 등 설정) | +| notice_items | body | array | 아니오 | max 50 | 상품정보제공고시 항목 배열 (각 항목: 항목명·내용 다국어) | +| label_assignments | body | array | 아니오 | — | 라벨 할당 배열 (label_id + 노출 시작/종료일로 상품에 라벨 부착) | +| min_purchase_qty | body | integer | 아니오 | min 1 | 최소 구매 수량 (1회 주문 시 이 수량 이상 구매) | +| max_purchase_qty | body | integer | 아니오 | min 0 | 최대 구매 수량 (0=무제한) | +| purchase_restriction | body | string | 아니오 | `none`, `restricted` | 구매 대상 제한 (none 제한 없음 / restricted 특정 역할만 구매 허용) | +| allowed_roles | body | array | 아니오 | — | 구매 허용 역할 ID 배열 (purchase_restriction=restricted 시 필수) | +| barcode | body | string | 아니오 | max 50 | 바코드 | +| hs_code | body | string | 아니오 | max 20 | HS 코드 (수출입 관세 분류 코드) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 상품코드로 기존 상품을 수정합니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, `ProductService::findByCode()`로 `code` 대상 상품을 찾은 뒤 `UpdateProductRequest`로 검증된 값을 `ProductService::update()`에 넘겨 상품·옵션·이미지·SEO 등을 갱신하고 `ProductResource`를 반환합니다. `{product}` ID 경로 대신 상품코드 기반으로 수정할 때 사용하며, 대상 상품이 없으면 404, 검증 실패는 422로 응답합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/products/generate-code + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.generate-code` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@generateCode` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.create` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.create`)이 없는 경우 | + + + +**설명** 중복되지 않는 신규 상품코드를 생성해 반환합니다. `auth:sanctum` + `sirsoft-ecommerce.products.create` 권한이 필요하며, `ProductService::generateUniqueCode()`가 기존 상품과 충돌하지 않는 코드를 발급해 `product_code` 필드로 응답합니다. 상품 등록 폼에서 코드 자동 채움 버튼을 눌렀을 때 사용하며, 요청 본문은 없습니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/products/images + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.images.upload-temp` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@uploadImage` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| file | body | file | 예 | max 10240 | 업로드 파일 | +| temp_key | body | string | 아니오 | max 64 | 임시 업로드 세션 키 (같은 상품의 여러 이미지를 한 세션으로 묶음, 생략 시 서버가 UUID 자동 발급) | +| collection | body | string | 아니오 | — | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) | +| alt_text | body | array | 아니오 | — | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-image.filter_upload_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 상품 등록 전 이미지를 임시로 업로드합니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, `productId` 경로가 없어 `ProductImageService::upload()`가 상품에 귀속되지 않은 임시 이미지로 저장합니다. `temp_key`를 넘기면 같은 업로드 세션으로 묶이고 생략 시 서버가 UUID를 자동 발급해 응답에 포함하므로, 이후 상품 생성/수정 요청의 `image_temp_key`로 전달해 실제 상품에 연결합니다. 컬렉션 내 첫 이미지는 자동으로 대표 이미지(`is_thumbnail`)로 지정되며, 개수 상한 초과 시 422를 반환합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/products/images/reorder + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.images.reorder` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@reorderImages` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | body | array | 예 | min 1 | 이미지 순서 배열 (각 항목: id + 부여할 order 값, 이미지별 노출 순서 갱신) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-image.filter_reorder_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 상품 이미지의 노출 순서를 일괄 변경합니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, 컨트롤러가 `order` 배열을 `id => order` 맵으로 변환해 `ProductImageService::reorder()`에 넘겨 각 이미지의 `sort_order`를 갱신합니다. 이미지 갤러리에서 드래그로 순서를 재배치한 결과를 저장할 때 사용합니다. 확장은 `sirsoft-ecommerce.product-image.filter_reorder_validation_rules` 훅으로 검증 규칙을 확장할 수 있습니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/products/images/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.images.delete` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@deleteImage` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 상품 이미지 1건을 삭제합니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, `ProductImageService::delete()`가 `id`에 해당하는 이미지 레코드와 저장 파일을 제거합니다. 임시 업로드 이미지와 상품에 귀속된 이미지 모두 삭제할 수 있으며, 해당 이미지가 없으면 404를 반환합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/products/{identifier} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| identifier | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자 화면용 단일 상품 상세를 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.products.read` 권한이 필요하며, `ProductService::findByIdOrCode()`로 숫자 ID 우선, 없으면 상품코드로 상품을 찾은 뒤 `getDetail($id, includeInactive: true)`로 비활성(숨김/판매중지) 상품까지 포함해 상세를 로드하고 `ProductResource`로 반환합니다. 공개 상세와 달리 전시상태에 관계없이 조회되므로 관리자 편집/열람에 사용하며, 대상이 없으면 404를 반환합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/products/{productId}/images + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.images.upload` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@uploadImage` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| productId | path | string | 예 | — | 대상 product의 식별자 | +| file | body | file | 예 | max 10240 | 업로드 파일 | +| temp_key | body | string | 아니오 | max 64 | 임시 업로드 세션 키 (같은 상품의 여러 이미지를 한 세션으로 묶음, 생략 시 서버가 UUID 자동 발급) | +| collection | body | string | 아니오 | — | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) | +| alt_text | body | array | 아니오 | — | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-image.filter_upload_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 기존 상품에 이미지 1건을 업로드해 즉시 귀속시킵니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, 경로의 `productId`가 있어 `ProductImageService::upload()`가 임시가 아닌 해당 상품 소유 이미지로 저장하고 `collection`별 마지막 순서에 추가합니다. 상품 편집 화면에서 이미지를 추가할 때 사용하며, 컬렉션의 첫 이미지는 자동으로 대표 이미지로 지정됩니다. 개수 상한 초과 시 422, 상품이 없으면 404를 반환합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/products/{productId}/images/{imageId}/thumbnail + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.images.set-thumbnail` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@setThumbnail` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| productId | path | string | 예 | — | 대상 product의 식별자 | +| imageId | path | string | 예 | — | 대상 image의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 상품의 대표(썸네일) 이미지를 지정합니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, `ProductImageService::setThumbnail()`가 같은 상품의 기존 대표 이미지의 `is_thumbnail`을 해제하고 `imageId` 이미지에 대표 플래그를 부여합니다. 목록·상세에서 노출될 기본 이미지를 교체할 때 사용하며, 지정 대상 상품/이미지가 없으면 404를 반환합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/products/{product} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| product | path | string | 예 | — | 대상 product의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 상품 1건을 삭제합니다. `auth:sanctum` + `sirsoft-ecommerce.products.delete` 권한이 필요하며, 먼저 `ProductService::checkCanDelete()`로 주문 이력을 선행 검사해 이력이 있으면 관련 주문 수(`count`)와 함께 409 Conflict로 차단하고, 통과 시 `ProductService::delete()`가 상품과 하위 데이터(옵션/이미지 등)를 명시적으로 제거합니다. 서비스 계층의 도메인 가드가 경합/우회 상황에서도 `ProductHasOrderHistoryException`으로 재차 409를 반환하며, 대상이 없으면 404를 반환합니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/products/{product} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| product | path | string | 예 | — | 대상 product의 식별자 | +| name | body | array | 아니오 | — | 대상의 이름/명칭 | +| product_code | body | string | 예 | max 50 | 상품코드 (상품 고유 관리 식별자, 상품 간 중복 불가) | +| sales_product_code | body | string | 아니오 | max 50 | 판매자 상품코드 (판매자가 직접 입력하는 관리용 코드) | +| sku | body | string | 아니오 | max 100 | 재고관리코드(SKU) | +| category_ids | body | array | 아니오 | min 1, max 5 | category 식별자 배열 | +| primary_category_id | body | integer | 아니오 | — | primary category 식별자 | +| brand_id | body | integer | 아니오 | — | brand 식별자 | +| list_price | body | integer | 아니오 | min 0.01 | 정가 (기본통화 기준, 소수 통화는 소수 입력 허용) | +| selling_price | body | integer | 아니오 | min 0.01 | 판매가 (기본통화 기준, 정가 이하여야 함) | +| stock_quantity | body | integer | 아니오 | min 0 | 재고 수량 (옵션 사용 시 옵션 재고 합계로 관리) | +| safe_stock_quantity | body | integer | 아니오 | min 0 | 안전재고 수량 (이 값 미만이면 재고 부족 표시) | +| sales_status | body | string | 아니오 | — | 판매상태 (on_sale 판매중 / suspended 판매중지 / sold_out 품절 / coming_soon 출시예정) | +| display_status | body | string | 아니오 | — | 전시상태 (visible 전시 / hidden 숨김) | +| tax_status | body | string | 아니오 | — | 과세여부 (taxable 과세 / tax_free 면세) | +| tax_rate | body | number | 아니오 | min 0, max 100 | 세율(%) (과세 상품의 부가세 계산 비율) | +| shipping_policy_id | body | integer | 아니오 | — | shipping policy 식별자 | +| common_info_id | body | integer | 아니오 | — | common info 식별자 | +| description | body | array | 아니오 | — | 설명 | +| description_mode | body | string | 아니오 | `text`, `html` | 상세 설명 편집 모드 (text 일반 텍스트 / html HTML 에디터) | +| thumbnail_hash | body | string | 아니오 | max 64 | 대표 이미지로 지정할 이미지 해시 (업로드된 이미지 중 썸네일 선택) | +| image_temp_key | body | string | 아니오 | max 64 | 임시 업로드 세션 키 (사전 업로드한 이미지를 이 상품에 연결) | +| images | body | array | 아니오 | max 20 | 상품 이미지 목록 (각 항목: id/hash/url/alt_text/is_thumbnail/sort_order) | +| meta_title | body | array | 아니오 | — | SEO 메타 제목 (검색엔진/소셜 공유 표시 제목) | +| meta_description | body | array | 아니오 | — | SEO 메타 설명 (검색엔진/소셜 공유 표시 요약) | +| meta_keywords | body | array | 아니오 | — | SEO 메타 키워드 배열 (검색엔진 색인용 키워드 목록) | +| seo_sync_title | body | boolean | 아니오 | — | SEO 제목 자동 동기화 여부 (true 시 상품명으로 메타 제목 자동 채움) | +| seo_sync_description | body | boolean | 아니오 | — | SEO 설명 자동 동기화 여부 (true 시 상품 설명으로 메타 설명 자동 채움) | +| use_main_image_for_og | body | boolean | 아니오 | — | 대표 이미지를 OG(소셜 공유) 이미지로 사용할지 여부 | +| has_options | body | boolean | 아니오 | — | options 여부 | +| option_groups | body | array | 아니오 | — | 옵션 그룹 정의 (예: 색상/사이즈 등 옵션 축과 각 축의 선택값 목록) | +| options | body | array | 아니오 | min 1 | 옵션(SKU) 목록 (각 항목: 옵션코드·옵션명·옵션값·정가·판매가·재고 등) | +| additional_options | body | array | 아니오 | max 5 | 추가옵션 그룹 배열 (각 그룹당 선택지 1~20개, 필수 여부·추가금·직접입력 허용 등 설정) | +| notice_items | body | array | 아니오 | max 50 | 상품정보제공고시 항목 배열 (각 항목: 항목명·내용 다국어) | +| label_assignments | body | array | 아니오 | — | 라벨 할당 배열 (label_id + 노출 시작/종료일로 상품에 라벨 부착) | +| min_purchase_qty | body | integer | 아니오 | min 1 | 최소 구매 수량 (1회 주문 시 이 수량 이상 구매) | +| max_purchase_qty | body | integer | 아니오 | min 0 | 최대 구매 수량 (0=무제한) | +| purchase_restriction | body | string | 아니오 | `none`, `restricted` | 구매 대상 제한 (none 제한 없음 / restricted 특정 역할만 구매 허용) | +| allowed_roles | body | array | 아니오 | — | 구매 허용 역할 ID 배열 (purchase_restriction=restricted 시 필수) | +| barcode | body | string | 아니오 | max 50 | 바코드 | +| hs_code | body | string | 아니오 | max 20 | HS 코드 (수출입 관세 분류 코드) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** ID 경로로 지정한 상품을 수정합니다. `auth:sanctum` + `sirsoft-ecommerce.products.update` 권한이 필요하며, 라우트 모델 바인딩된 `Product`에 `UpdateProductRequest`로 검증된 값을 `ProductService::update()`로 반영해 상품 기본정보·옵션·이미지·SEO 메타를 갱신하고 `ProductResource`를 반환합니다. `by-code` 변형과 동일 서비스 메서드를 쓰지만 상품코드 조회 단계 없이 바로 대상 모델을 받습니다. 검증 실패는 422, 대상이 없으면 404로 응답합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/products/{product}/can-delete + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.can-delete` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@canDelete` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| product | path | string | 예 | — | 대상 product의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| canDelete | boolean | `false` | 삭제 가능 여부 (true 삭제 가능 / false 주문 이력 등으로 삭제 불가) | +| reason | string | `이 상품은 5건의 주문 이력이 있어 삭제할 수 없습니다.` | 삭제 불가 사유 (canDelete=false 일 때 안내 문구) | +| relatedData | object | `{"orders":5,"images":4,"options":3,"additionalOptions":0,…` | 연관 데이터 건수 (orders/images/options 등 상품에 연결된 하위 데이터 수) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 상품 삭제 가능 여부를 사전 확인합니다. `auth:sanctum` + `sirsoft-ecommerce.products.delete` 권한이 필요하며, `ProductService::checkCanDelete()`가 주문 이력 등 연관 데이터를 검사해 `canDelete` 불리언과 차단 `reason`, 그리고 `relatedData`(orders/images/options 등 연관 건수)를 반환합니다. 삭제 버튼을 누르기 전 확인 다이얼로그에서 삭제 가능 여부와 연관 데이터를 안내하는 데 사용하며, 실제 삭제는 DELETE 엔드포인트가 수행합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/products/{product}/copy + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.show-for-copy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@showForCopy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| product | path | string | 예 | — | 대상 product의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| name | object | `{"ko":"면 손수건 3매입 #1","en":"Cotton Handkerchief 3pcs #1"}` | 상품명 (다국어 JSON: {ko: "...", en: "..."}) | +| product_code | string | `BP747N3QSSSNEPII` | 상품코드 | +| sales_product_code | null | `null` | 판매자 상품코드 (사용자 입력용) | +| sku | string | `HK-0001` | SKU | +| brand_id | integer | `91` | 브랜드 ID | +| category_ids | array | `[114,116]` | category 식별자 배열 (연관 리소스 참조) | +| primary_category_id | integer | `116` | primary category 식별자 (연관 리소스 참조) | +| list_price | string | `4000.00` | 정가 (기본통화 기준) | +| selling_price | string | `2000.00` | 판매가 (기본통화 기준) | +| stock_quantity | integer | `59` | 재고 수량 (옵션 있으면 옵션 합계) | +| safe_stock_quantity | integer | `15` | 안전재고 수량 | +| tax_status | string | `taxable` | 과세여부: taxable(과세), tax_free(면세) | +| sales_status | string | `on_sale` | 판매상태: on_sale(판매중), suspended(판매중지), sold_out(품절), coming_soon(출시예정) | +| display_status | string | `visible` | 전시상태: visible(전시), hidden(숨김) | +| options | array | `[{"option_code":"AS2HM7CEDFHEGS43-001","option_name":{"ko…` | 복사 대상 옵션(SKU) 목록 (신규 등록 폼에 채울 옵션 정의, 다중통화 가격 포함) | +| additional_options | array | `[]` | 복사 대상 추가옵션 그룹 목록 (그룹명·선택지·추가금 등) | +| images | array | `[{"hash":"7858be3cf217","url":null,"original_filename":"p…` | 복사 대상 이미지 목록 (각 항목: hash·원본파일명 등, copy_images 선택 시 포함) | +| thumbnail_hash | string | `7858be3cf217` | 대표 이미지 해시 (썸네일로 지정된 이미지의 hash) | +| description | object | `{"ko":"

부드러운 면 100% 손수건 3매 세트입니다.<\/p>","en":"

A set …` | 상세 설명 (다국어 JSON, HTML 포함) | +| description_mode | string | `text` | 설명 모드: text(텍스트), html(HTML) | +| notice_items | array | `[{"name":{"ko":"제품 소재 (충전재 포함)","en":"Material (Including…` | 상품정보제공고시 항목 목록 (각 항목: 항목명·내용 다국어) | +| shipping_policy_id | integer | `31` | 배송정책 ID | +| shipping_policy | object | `{"id":31,"name":{"ko":"국내 무료배송","en":"Domestic Free Shipp…` | 현재 부여된 배송정책 객체 (비활성 포함 — 수정폼 활성 목록에 없을 때 union 표시용) | +| common_info_id | integer | `207` | 공통정보 템플릿 ID | +| label_assignments | array | `[{"label_id":26,"start_date":null,"end_date":null}]` | 라벨 할당 목록 (각 항목: label_id + 노출 시작/종료일) | +| min_purchase_qty | integer | `1` | 최소 구매 수량 | +| max_purchase_qty | integer | `0` | 최대 구매 수량 (0=무제한) | +| purchase_restriction | string | `none` | 구매 제한: none(없음), restricted(제한) | +| allowed_roles | array | `[]` | 구매 허용 역할 ID 배열 | +| meta_title | null | `null` | SEO 제목 (다국어 JSON) | +| meta_description | null | `null` | SEO 설명 (다국어 JSON) | +| seo_tags | array | `[]` | SEO 태그 목록 (메타 키워드 등 검색엔진 노출용 태그) | +| seo_sync_title | boolean | `true` | SEO 제목 동기화 여부 (1: 상품명으로 자동 채움, 0: 직접 입력 보존) | +| seo_sync_description | boolean | `true` | SEO 설명 동기화 여부 (1: 상품 설명으로 자동 채움, 0: 직접 입력 보존) | +| barcode | null | `null` | 바코드 | +| hs_code | null | `null` | HS 코드 (관세 분류) | +| thumbnail_url | string | `/api/modules/sirsoft-ecommerce/produc…` | thumbnail URL | +| categories | array | `[{"id":114,"name":{"ko":"스포츠","en":"Sports"},"name_locali…` | 소속 카테고리 목록 (breadcrumb 포함 — 복사 폼의 카테고리 표시용) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 상품 복사(복제) 등록 폼을 채우기 위한 원본 데이터를 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.products.read` 권한이 필요하며, `copy_images`/`copy_options`/`copy_categories`/`copy_seo` 등 쿼리 불리언으로 복사 항목을 선택하면 `ProductService::getDetailForCopy()`가 해당 항목만 담아 반환합니다. 컨트롤러가 대표 이미지의 `thumbnail_url`, 카테고리 breadcrumb, 옵션별 다중통화 가격(`ProductOptionResource`)을 추가로 보강하며, SEO는 기본적으로 복사 제외(false)입니다. 대상 상품이 없으면 404를 반환합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/products/{product}/form + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.show-for-form` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@showForForm` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| product | path | string | 예 | — | 대상 product의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `201` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"면 손수건 3매입 #1","en":"Cotton Handkerchief 3pcs #1"}` | 상품명 (다국어 JSON: {ko: "...", en: "..."}) | +| product_code | string | `AS2HM7CEDFHEGS43` | 상품코드 | +| sales_product_code | null | `null` | 판매자 상품코드 (사용자 입력용) | +| sku | string | `HK-0001` | SKU | +| brand_id | integer | `91` | 브랜드 ID | +| category_ids | array | `[114,116]` | category 식별자 배열 (연관 리소스 참조) | +| primary_category_id | integer | `116` | primary category 식별자 (연관 리소스 참조) | +| created_at | string | `2026-06-15 02:24:15` | 생성 일시 | +| updated_at | string | `2026-06-15 02:24:15` | 최종 수정 일시 | +| list_price | string | `4000.00` | 정가 (기본통화 기준) | +| selling_price | string | `2000.00` | 판매가 (기본통화 기준) | +| stock_quantity | integer | `59` | 재고 수량 (옵션 있으면 옵션 합계) | +| safe_stock_quantity | integer | `15` | 안전재고 수량 | +| tax_status | string | `taxable` | 과세여부: taxable(과세), tax_free(면세) | +| sales_status | string | `on_sale` | 판매상태: on_sale(판매중), suspended(판매중지), sold_out(품절), coming_soon(출시예정) | +| display_status | string | `visible` | 전시상태: visible(전시), hidden(숨김) | +| options | array | `[{"id":1086,"option_code":"AS2HM7CEDFHEGS43-001","option_…` | 옵션(SKU) 목록 (수정 폼 바인딩용, 각 옵션의 id·코드·옵션값·가격·재고 등) | +| additional_options | array | `[]` | 추가옵션 그룹 목록 (수정 폼 바인딩용, 그룹명·선택지·추가금 등) | +| images | array | `[{"id":801,"hash":"7858be3cf217","url":null,"original_fil…` | 이미지 목록 (각 항목: id·hash·원본파일명 등) | +| thumbnail_hash | string | `7858be3cf217` | 대표 이미지 해시 (썸네일로 지정된 이미지의 hash) | +| description | object | `{"ko":"

부드러운 면 100% 손수건 3매 세트입니다.<\/p>","en":"

A set …` | 상세 설명 (다국어 JSON, HTML 포함) | +| description_mode | string | `text` | 설명 모드: text(텍스트), html(HTML) | +| notice_items | array | `[{"name":{"ko":"제품 소재 (충전재 포함)","en":"Material (Including…` | 상품정보제공고시 항목 목록 (각 항목: 항목명·내용 다국어) | +| shipping_policy_id | integer | `31` | 배송정책 ID | +| shipping_policy | object | `{"id":31,"name":{"ko":"국내 무료배송","en":"Domestic Free Shipp…` | 현재 부여된 배송정책 객체 (비활성 포함 — 수정폼 활성 목록에 없을 때 union 표시용) | +| common_info_id | integer | `207` | 공통정보 템플릿 ID | +| label_assignments | array | `[{"label_id":26,"start_date":null,"end_date":null}]` | 라벨 할당 목록 (각 항목: label_id + 노출 시작/종료일) | +| min_purchase_qty | integer | `1` | 최소 구매 수량 | +| max_purchase_qty | integer | `0` | 최대 구매 수량 (0=무제한) | +| purchase_restriction | string | `none` | 구매 제한: none(없음), restricted(제한) | +| allowed_roles | array | `[]` | 구매 허용 역할 ID 배열 | +| meta_title | null | `null` | SEO 제목 (다국어 JSON) | +| meta_description | null | `null` | SEO 설명 (다국어 JSON) | +| seo_tags | array | `[]` | SEO 태그 목록 (메타 키워드 등 검색엔진 노출용 태그) | +| seo_sync_title | boolean | `true` | SEO 제목 동기화 여부 (1: 상품명으로 자동 채움, 0: 직접 입력 보존) | +| seo_sync_description | boolean | `true` | SEO 설명 동기화 여부 (1: 상품 설명으로 자동 채움, 0: 직접 입력 보존) | +| barcode | null | `null` | 바코드 | +| hs_code | null | `null` | HS 코드 (관세 분류) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 상품 수정 폼을 채우기 위한 상세 데이터를 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.products.read` 권한이 필요하며, `ProductService::getDetailForForm()`가 폼 입력 필드에 맞춘 형태(카테고리 ID 배열, 옵션/추가옵션, 이미지, SEO 태그 등)로 데이터를 반환합니다. 관리자 상품 편집 화면 진입 시 폼 초기값을 로드하는 데 사용하며, 대상 상품이 없으면 404를 반환합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/products/{product}/logs + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.products.logs` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductController@logs` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| product | path | string | 예 | — | 대상 product의 식별자 | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `151066` | 기본 키 (내부 식별자) | +| log_type | string | `admin` | 로그 구분 값 (admin 관리자 작업 / user 사용자 작업 등 활동 로그 채널) | +| log_type_label | string | `관리자` | `log_type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| loggable_type | string | `Modules\Sirsoft\Ecommerce\Models\Product` | 로그 대상 모델의 전체 클래스명 (상품 또는 상품옵션 모델) | +| loggable_type_display | string | `Product` | 로그 대상 모델의 표시용 짧은 이름 (클래스 basename) | +| loggable_id | integer | `201` | loggable 식별자 (연관 리소스 참조) | +| action | string | `product.create` | 활동 액션 키 (product.create/update 등 수행된 작업 식별자) | +| action_label | string | `생성` | `action` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| localized_description | string | `상품 생성 (면 손수건 3매입 #1)` | `description` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| description_key | string | `sirsoft-ecommerce::activity_log.descr…` | 설명 번역 키 (localized_description 을 생성하는 다국어 키) | +| properties | null | `null` | 로그 부가 속성 (액션에 첨부된 임의 메타데이터, 없으면 null) | +| changes | array | `[{"field":"sku","label_key":"sirsoft-ecommerce::activity_…` | 단일 수정 변경 내역 (각 항목: field·label·old·new, 일괄 수정 로그면 null) | +| bulk_changes | null | `null` | 일괄 수정 변경 내역 (각 항목: model_id·changes 배열, 단일 수정 로그면 null) | +| has_changes | boolean | `false` | changes 여부 | +| actor_name | string | `관리자` | 행위를 수행한 주체(사용자/시스템)의 이름 | +| user | object | `{"uuid":"a1e0a91a-fba6-491c-a53e-7285a5686857","name":"관리…` | 행위 수행 사용자 정보 (uuid·name·email, 시스템 작업이면 name 만 '시스템') | +| ip_address | string | `192.168.1.10` | 요청/행위가 발생한 IP 주소 | +| created_at | string | `2026-06-14 08:28:44` | 생성 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_read":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 상품의 처리로그(활동 로그) 목록을 조회합니다. `auth:sanctum` + `sirsoft-ecommerce.products.read` 권한이 필요하며, 컨트롤러가 해당 상품과 그 하위 옵션(`ProductOption`)의 `ActivityLog` 레코드를 `loggable_type`/`loggable_id` 기준으로 합쳐 `created_at` 정렬(기본 desc)로 페이지네이션합니다. `per_page`/`sort_order` 쿼리로 조회 범위를 조정하며, 상품 상세의 처리 이력 탭에서 생성/수정/재고 변경 등 감사 로그를 표시하는 데 사용합니다. 대상 상품이 없으면 404를 반환합니다. + + +### GET /api/modules/sirsoft-ecommerce/products + +- **라우트명**: `api.modules.sirsoft-ecommerce.products.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\ProductController@index` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-ecommerce.user-products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| category_id | query | integer | 아니오 | — | category 식별자 | +| category_slug | query | string | 아니오 | max 100 | 카테고리 slug 필터 (URL 친화 식별자로 카테고리 지정, category_id 대체 가능) | +| brand_id | query | integer | 아니오 | — | brand 식별자 | +| search | query | string | 아니오 | max 200 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| sort | query | string | 아니오 | `latest`, `sales`, `price_asc`, `price_desc` | 정렬 기준 (필드명, `-` 접두 시 내림차순) | +| min_price | query | integer | 아니오 | min 0 | 판매가 범위 필터 하한 (판매가가 이 값 이상인 상품) | +| max_price | query | integer | 아니오 | min 0 | 판매가 범위 필터 상한 (판매가가 이 값 이하인 상품) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.public_list_validation_rules`, `sirsoft-ecommerce.product.public_list_validation_messages`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `109` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `322` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"eum et quia","en":"tenetur id quae"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| name_localized | string | `eum et quia` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| product_code | string | `PROD-GJUX-1484` | 상품코드 (상품 고유 관리 식별자) | +| sku | string | `SKU-MRAD-9306` | 재고관리코드(SKU) | +| thumbnail_url | string | `/api/modules/sirsoft-ecommerce/produc…` | thumbnail URL | +| list_price | integer | `112594` | 정가 (기본통화 자릿수로 정규화된 값) | +| list_price_formatted | string | `112,594원` | `list_price` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| selling_price | integer | `88949` | 판매가 (기본통화 자릿수로 정규화된 값) | +| selling_price_formatted | string | `88,949원` | `selling_price` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| discount_rate | integer | `21` | 할인율(%) (정가 대비 판매가 할인 비율, (1 - 판매가/정가) × 100) | +| multi_currency_list_price | object | `{"KRW":{"price":112594,"formatted":"112,594원","is_default…` | 통화별 정가 맵 (통화코드 → {price, formatted, is_default, editable}, 설정된 모든 통화의 환산 정가) | +| multi_currency_selling_price | object | `{"KRW":{"price":88949,"formatted":"88,949원","is_default":…` | 통화별 판매가 맵 (통화코드 → {price, formatted, is_default, editable}, 설정된 모든 통화의 환산 판매가) | +| stock_quantity | integer | `22` | 재고 수량 (옵션 사용 시 옵션 재고 합계) | +| safe_stock_quantity | integer | `12` | 안전재고 수량 (이 값 미만이면 재고 부족으로 표시) | +| is_below_safe_stock | boolean | `false` | below safe stock 여부 | +| sales_status | string | `on_sale` | 판매상태 값 (on_sale 판매중 / suspended 판매중지 / sold_out 품절 / coming_soon 출시예정) | +| sales_status_label | string | `판매중` | `sales_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| sales_status_variant | string | `success` | `sales_status` 값의 표시 변형 키 (UI 배지 색상/스타일) | +| display_status | string | `visible` | 전시상태 값 (visible 전시 / hidden 숨김) | +| display_status_label | string | `전시` | `display_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| display_status_variant | string | `success` | `display_status` 값의 표시 변형 키 (UI 배지 색상/스타일) | +| categories | array | `[]` | 소속 카테고리 목록 (각 항목: id·현지화 이름·대표 여부. categories 관계 eager load 시에만 채워짐) | +| primary_category | string | `스마트폰` | 대표 카테고리명 (is_primary 카테고리의 현지화 이름) | +| categories_with_path | array | `[]` | 소속 카테고리 목록 + 경로 (각 항목: id·breadcrumb path·대표 여부) | +| brand_name | string | `CJ제일제당` | 브랜드명 (연관 브랜드의 현지화 이름) | +| shipping_policy_id | integer | `31` | shipping policy 식별자 (연관 리소스 참조) | +| min_purchase_qty | integer | `1` | 최소 구매 수량 (1회 주문 시 이 수량 이상 구매) | +| max_purchase_qty | integer | `0` | 최대 구매 수량 (0=무제한) | +| has_options | boolean | `false` | options 여부 | +| labels | array | `[]` | 노출 중인 상품 라벨 목록 (각 항목: 라벨명·색상, 활성 라벨을 sort_order 순으로 정렬) | +| review_count | integer | `0` | review 개수 (집계) | +| rating_avg | integer | `0` | 평균 별점 (공개 리뷰 별점 평균, 소수 1자리 반올림) | +| created_at | string | `2026-07-07 14:47:31` | 생성 일시 | +| updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | +| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-products.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 쇼핑몰 프런트용 공개 상품 목록을 조회합니다. `optional.sanctum`(회원/비회원 모두 접근) + `sirsoft-ecommerce.user-products.read` 권한이 적용되며, `ProductService::getPublicList()`가 전시상태 visible이고 판매상태가 on_sale 또는 coming_soon인 상품만 반환합니다. 카테고리/브랜드/검색어/가격 범위 필터와 `sort`(latest/sales/price_asc/price_desc) 정렬을 지원하고 결과는 `ProductCollection`으로 페이지네이션됩니다. 확장은 `sirsoft-ecommerce.product.public_list_validation_rules` 훅으로 필터를 추가할 수 있습니다. + + +### GET /api/modules/sirsoft-ecommerce/products/new + +- **라우트명**: `api.modules.sirsoft-ecommerce.products.new` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\ProductController@new` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-ecommerce.user-products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| limit | query | integer | 아니오 | min 1, max 50 | 반환할 최대 항목 수 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.public_new_validation_rules`). + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `322` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"eum et quia","en":"tenetur id quae"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| name_localized | string | `eum et quia` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| product_code | string | `PROD-GJUX-1484` | 상품코드 (상품 고유 관리 식별자) | +| sku | string | `SKU-MRAD-9306` | 재고관리코드(SKU) | +| thumbnail_url | string | `/api/modules/sirsoft-ecommerce/produc…` | thumbnail URL | +| list_price | integer | `112594` | 정가 (기본통화 자릿수로 정규화된 값) | +| list_price_formatted | string | `112,594원` | `list_price` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| selling_price | integer | `88949` | 판매가 (기본통화 자릿수로 정규화된 값) | +| selling_price_formatted | string | `88,949원` | `selling_price` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| discount_rate | integer | `21` | 할인율(%) (정가 대비 판매가 할인 비율, (1 - 판매가/정가) × 100) | +| multi_currency_list_price | object | `{"KRW":{"price":112594,"formatted":"112,594원","is_default…` | 통화별 정가 맵 (통화코드 → {price, formatted, is_default, editable}, 설정된 모든 통화의 환산 정가) | +| multi_currency_selling_price | object | `{"KRW":{"price":88949,"formatted":"88,949원","is_default":…` | 통화별 판매가 맵 (통화코드 → {price, formatted, is_default, editable}, 설정된 모든 통화의 환산 판매가) | +| stock_quantity | integer | `22` | 재고 수량 (옵션 사용 시 옵션 재고 합계) | +| safe_stock_quantity | integer | `12` | 안전재고 수량 (이 값 미만이면 재고 부족으로 표시) | +| is_below_safe_stock | boolean | `false` | below safe stock 여부 | +| sales_status | string | `on_sale` | 판매상태 값 (on_sale 판매중 / suspended 판매중지 / sold_out 품절 / coming_soon 출시예정) | +| sales_status_label | string | `판매중` | `sales_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| sales_status_variant | string | `success` | `sales_status` 값의 표시 변형 키 (UI 배지 색상/스타일) | +| display_status | string | `visible` | 전시상태 값 (visible 전시 / hidden 숨김) | +| display_status_label | string | `전시` | `display_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| display_status_variant | string | `success` | `display_status` 값의 표시 변형 키 (UI 배지 색상/스타일) | +| categories | array | `[]` | 소속 카테고리 목록 (각 항목: id·현지화 이름·대표 여부. categories 관계 eager load 시에만 채워짐) | +| primary_category | string | `스마트폰` | 대표 카테고리명 (is_primary 카테고리의 현지화 이름) | +| categories_with_path | array | `[]` | 소속 카테고리 목록 + 경로 (각 항목: id·breadcrumb path·대표 여부) | +| shipping_policy_id | integer | `31` | shipping policy 식별자 (연관 리소스 참조) | +| min_purchase_qty | integer | `1` | 최소 구매 수량 (1회 주문 시 이 수량 이상 구매) | +| max_purchase_qty | integer | `0` | 최대 구매 수량 (0=무제한) | +| has_options | boolean | `false` | options 여부 | +| labels | array | `[]` | 노출 중인 상품 라벨 목록 (각 항목: 라벨명·색상, 활성 라벨을 sort_order 순으로 정렬) | +| review_count | integer | `0` | review 개수 (집계) | +| rating_avg | integer | `0` | 평균 별점 (공개 리뷰 별점 평균, 소수 1자리 반올림) | +| created_at | string | `2026-07-07 14:47:31` | 생성 일시 | +| updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | +| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-products.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 공개 신상품 목록을 조회합니다. `optional.sanctum`(회원/비회원 모두 접근) + `sirsoft-ecommerce.user-products.read` 권한이 적용되며, `ProductService::getNewProducts()`가 최신 등록순으로 상품을 정렬해 `ProductListResource` 컬렉션으로 반환합니다. `limit`(기본 10, 최대 50) 쿼리로 개수를 제한하며, 메인 페이지의 신상품 섹션 등에 사용됩니다. 확장은 `sirsoft-ecommerce.product.public_new_validation_rules` 훅으로 검증 규칙을 확장할 수 있습니다. + + +### GET /api/modules/sirsoft-ecommerce/products/popular + +- **라우트명**: `api.modules.sirsoft-ecommerce.products.popular` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\ProductController@popular` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-ecommerce.user-products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| limit | query | integer | 아니오 | min 1, max 50 | 반환할 최대 항목 수 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.public_popular_validation_rules`). + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `204` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"베이직 라운드 티셔츠 #4","en":"Basic Round T-Shirt #4"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| name_localized | string | `베이직 라운드 티셔츠 #4` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| product_code | string | `2R9AKHR0GH2DR3NG` | 상품코드 (상품 고유 관리 식별자) | +| sku | string | `TS-0004` | 재고관리코드(SKU) | +| thumbnail_url | string | `/api/modules/sirsoft-ecommerce/produc…` | thumbnail URL | +| list_price | integer | `29000` | 정가 (기본통화 자릿수로 정규화된 값) | +| list_price_formatted | string | `29,000원` | `list_price` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| selling_price | integer | `22000` | 판매가 (기본통화 자릿수로 정규화된 값) | +| selling_price_formatted | string | `22,000원` | `selling_price` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| discount_rate | number | `24.1` | 할인율(%) (정가 대비 판매가 할인 비율, (1 - 판매가/정가) × 100) | +| multi_currency_list_price | object | `{"KRW":{"price":29000,"formatted":"29,000원","is_default":…` | 통화별 정가 맵 (통화코드 → {price, formatted, is_default, editable}, 설정된 모든 통화의 환산 정가) | +| multi_currency_selling_price | object | `{"KRW":{"price":22000,"formatted":"22,000원","is_default":…` | 통화별 판매가 맵 (통화코드 → {price, formatted, is_default, editable}, 설정된 모든 통화의 환산 판매가) | +| stock_quantity | integer | `264` | 재고 수량 (옵션 사용 시 옵션 재고 합계) | +| safe_stock_quantity | integer | `15` | 안전재고 수량 (이 값 미만이면 재고 부족으로 표시) | +| is_below_safe_stock | boolean | `false` | below safe stock 여부 | +| sales_status | string | `on_sale` | 판매상태 값 (on_sale 판매중 / suspended 판매중지 / sold_out 품절 / coming_soon 출시예정) | +| sales_status_label | string | `판매중` | `sales_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| sales_status_variant | string | `success` | `sales_status` 값의 표시 변형 키 (UI 배지 색상/스타일) | +| display_status | string | `visible` | 전시상태 값 (visible 전시 / hidden 숨김) | +| display_status_label | string | `전시` | `display_status` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| display_status_variant | string | `success` | `display_status` 값의 표시 변형 키 (UI 배지 색상/스타일) | +| categories | array | `[{"id":109,"name":"식품","is_primary":0},{"id":113,"name":"…` | 소속 카테고리 목록 (각 항목: id·현지화 이름·대표 여부) | +| primary_category | string | `해산물` | 대표 카테고리명 (is_primary 카테고리의 현지화 이름) | +| categories_with_path | array | `[{"id":109,"path":[{"id":109,"name":"식품","slug":"food"}],…` | 소속 카테고리 목록 + 경로 (각 항목: id·breadcrumb path·대표 여부) | +| shipping_policy_id | integer | `31` | shipping policy 식별자 (연관 리소스 참조) | +| min_purchase_qty | integer | `1` | 최소 구매 수량 (1회 주문 시 이 수량 이상 구매) | +| max_purchase_qty | integer | `0` | 최대 구매 수량 (0=무제한) | +| has_options | boolean | `true` | options 여부 | +| labels | array | `[]` | 노출 중인 상품 라벨 목록 (각 항목: 라벨명·색상, 활성 라벨을 sort_order 순으로 정렬) | +| review_count | integer | `0` | review 개수 (집계) | +| rating_avg | integer | `0` | 평균 별점 (공개 리뷰 별점 평균, 소수 1자리 반올림) | +| created_at | string | `2026-06-15 11:24:15` | 생성 일시 | +| updated_at | string | `2026-06-15 11:24:15` | 최종 수정 일시 | +| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-products.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 공개 인기 상품 목록을 조회합니다. `optional.sanctum`(회원/비회원 모두 접근) + `sirsoft-ecommerce.user-products.read` 권한이 적용되며, `ProductService::getPopularProducts()`가 최근 30일 판매량 기준으로 정렬한 상품을 `ProductListResource` 컬렉션으로 반환합니다. `limit`(기본 10, 최대 50) 쿼리로 개수를 제한하며, 베스트/인기 상품 위젯에 사용됩니다. 확장은 `sirsoft-ecommerce.product.public_popular_validation_rules` 훅으로 검증 규칙을 확장할 수 있습니다. + + +### GET /api/modules/sirsoft-ecommerce/products/recent + +- **라우트명**: `api.modules.sirsoft-ecommerce.products.recent` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\ProductController@recent` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-ecommerce.user-products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | query | string | 아니오 | max 500 | 대상 리소스 식별자 배열 (대량 작업 대상) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.public_recent_validation_rules`). + +**응답 필드** (`data` 내부) + + + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-products.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 최근 본 상품 목록을 조회합니다. `optional.sanctum`(회원/비회원 모두 접근) + `sirsoft-ecommerce.user-products.read` 권한이 적용되며, 클라이언트가 로컬에 보관한 조회 이력 상품 ID들을 쉼표 구분 문자열(`ids`)로 전달하면 컨트롤러가 정수 배열로 파싱해 `ProductService::getProductsByIds()`로 조회한 뒤 `ProductListResource` 컬렉션을 반환합니다. `ids`가 비어 있으면 빈 배열을 반환하며, 확장은 `sirsoft-ecommerce.product.public_recent_validation_rules` 훅으로 검증 규칙을 확장할 수 있습니다. + + +### GET /api/modules/sirsoft-ecommerce/products/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.products.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\ProductController@show` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-ecommerce.user-products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-products.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 쇼핑몰 프런트용 공개 상품 상세를 조회합니다. `optional.sanctum`(회원/비회원 모두 접근) + `sirsoft-ecommerce.user-products.read` 권한이 적용되며, `ProductService::getDetail()`로 상품을 로드하되 전시상태가 visible이 아니면 404를 반환합니다. 상세 페이지에 필요한 배송정책·상품고시·공통정보·브랜드·라벨·추가옵션·현재 사용자 위시리스트 관계를 추가 로드하고 `PublicProductResource`로 반환하며, 응답에는 다중통화 가격·배송비 안내(`shipping_fee_formatted`)·`is_wishlisted` 등 프런트 표시용 파생 필드가 포함됩니다. + + +### GET /api/modules/sirsoft-ecommerce/products/{productId}/downloadable-coupons + +- **라우트명**: `api.modules.sirsoft-ecommerce.products.downloadable-coupons` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\PublicCouponController@downloadableCoupons` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-ecommerce.user-products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| productId | path | string | 예 | — | 대상 product의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-products.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 상품에서 다운로드 가능한 쿠폰 목록을 조회합니다. `optional.sanctum`(회원/비회원 모두 접근) + `sirsoft-ecommerce.user-products.read` 권한이 적용되며, `UserCouponService::getProductDownloadableCoupons()`가 해당 상품에 적용 가능한 발급 대기 쿠폰을 반환합니다. 로그인 상태면 사용자 ID를 함께 넘겨 각 쿠폰의 `is_downloaded`(이미 받았는지) 여부를 채워주고, 다중통화 혜택·최소주문금액(`multi_currency_benefit_formatted` 등)이 포함됩니다. 상품 상세의 쿠폰 받기 영역에 사용됩니다. + + +### GET /api/modules/sirsoft-ecommerce/products/{productId}/inquiries + +- **라우트명**: `api.modules.sirsoft-ecommerce.products.inquiries.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\ProductInquiryController@index` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-ecommerce.user-products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| productId | path | string | 예 | — | 대상 product의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-products.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 상품의 1:1 문의 목록을 조회합니다. `optional.sanctum`(회원/비회원 모두 접근) + `sirsoft-ecommerce.user-products.read` 권한이 적용되며, `ProductInquiryService::getProductInquiries()`가 게시판 모듈과 연동된 문의 글을 페이지네이션해 `items`와 `board_settings`(비밀글 모드·카테고리 등) 메타를 반환합니다. `per_page`/`page`/`exclude_secret` 쿼리로 조회 범위를 조정하며, 비밀 문의는 설정과 열람 권한에 따라 마스킹됩니다. 상품 상세의 문의 탭에 사용됩니다. + + +### POST /api/modules/sirsoft-ecommerce/products/{productId}/inquiries + +- **라우트명**: `api.modules.sirsoft-ecommerce.products.inquiries.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\ProductInquiryController@store` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-ecommerce.user-products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| productId | path | string | 예 | — | 대상 product의 식별자 | +| title | body | string | 아니오 | — | 제목 | +| category | body | string | 아니오 | — | 문의 분류 (게시판 설정에 정의된 카테고리, 미지정 시 기본값) | +| content | body | string | 예 | — | 본문 내용 | +| is_secret | body | boolean | 아니오 | — | secret 여부 | +| temp_key | body | string | 아니오 | — | 첨부파일 임시 업로드 키 (사전 업로드한 첨부를 이 문의에 연결) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.inquiry.store_validation_rules`, `sirsoft-ecommerce.inquiry.store_validation_messages`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-products.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 상품에 1:1 문의를 작성합니다. `optional.sanctum` + `sirsoft-ecommerce.user-products.read` 권한이 적용되며(선택적 인증 표면이지만 실제 작성은 인증 사용자를 전제), `ProductInquiryService::createInquiry()`가 게시판 모듈과 연동해 문의 글을 생성하고 성공 시 201과 생성된 `id`를 반환합니다. `content`는 필수, `title`/`category`/`is_secret`은 선택이며, 첨부는 사전 업로드한 `temp_key`로 연결됩니다. 도메인 규칙 위반(비밀글 비허용 등)은 `RuntimeException`으로 422를 반환합니다. + + +### GET /api/modules/sirsoft-ecommerce/products/{productId}/reviews + +- **라우트명**: `api.modules.sirsoft-ecommerce.products.reviews.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\ProductReviewController@index` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:sirsoft-ecommerce.user-products.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| productId | path | string | 예 | — | 대상 product의 식별자 | +| sort | query | string | 아니오 | `created_at_desc`, `created_at_asc`, `rating_desc`, `rating_asc` | 정렬 기준 (필드명, `-` 접두 시 내림차순) | +| photo_only | query | string | 아니오 | `0`, `1`, `true`, `false` | 포토리뷰만 필터 (true 시 사진이 첨부된 리뷰만 조회) | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| per_page | query | integer | 아니오 | min 1, max 50 | 페이지당 항목 수 | +| rating | query | integer | 아니오 | `1`, `2`, `3`, `4`, `5` | 별점 필터 (지정한 평점의 리뷰만 조회) | +| option_filters | query | string | 아니오 | — | 옵션 조건 필터 (JSON 문자열로 전달, 서버에서 배열로 파싱해 특정 옵션 구매 리뷰만 조회) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.review.public_list_validation_rules`, `sirsoft-ecommerce.review.public_list_validation_messages`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-products.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 상품의 공개 리뷰 목록과 별점 통계를 조회합니다. `optional.sanctum`(회원/비회원 모두 접근) + `sirsoft-ecommerce.user-products.read` 권한이 적용되며, `ProductReviewService::getProductReviews()`가 정렬(`sort`)·포토리뷰만(`photo_only`)·별점(`rating`)·옵션(`option_filters`) 필터를 적용해 리뷰를 페이지네이션하고 별점 분포(`rating_stats`)와 선택 가능한 옵션 필터, 총 개수를 함께 반환합니다. `option_filters`는 JSON 문자열로 전달되면 서버에서 배열로 파싱되며, 상품 상세의 리뷰 탭에 사용됩니다. 확장은 `sirsoft-ecommerce.review.public_list_validation_rules` 훅으로 필터를 추가할 수 있습니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/promotion-coupons.md b/modules/_bundled/sirsoft-ecommerce/docs/api/promotion-coupons.md new file mode 100644 index 00000000..f93ecfaa --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/promotion-coupons.md @@ -0,0 +1,411 @@ +# Promotion Coupons API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Promotion Coupons 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/promotion-coupons + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.promotion-coupons.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CouponController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.promotion-coupon.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| sort_by | query | string | 아니오 | `created_at`, `name`, `discount_value`, `issued_count` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| search_field | query | string | 아니오 | `all`, `name`, `description`, `created_by` | 검색 대상 필드명 (검색어를 적용할 컬럼) | +| search_keyword | query | string | 아니오 | max 255 | 검색 키워드 (부분 일치) | +| target_type | query | string | 아니오 | `all`, `product_amount`, `order_amount`, `shipping_fee` | 적용대상(할인 기준) 필터: 상품금액/주문금액/배송비 (`all`=전체) | +| discount_type | query | string | 아니오 | `all`, `fixed`, `rate` | 혜택유형 필터: fixed(정액), rate(정률%) (`all`=전체) | +| issue_status | query | string | 아니오 | `all`, `issuing`, `stopped` | 발급상태 필터: issuing(발급중), stopped(발급중단) (`all`=전체) | +| issue_method | query | string | 아니오 | `all`, `direct`, `download`, `auto` | 발급방법 필터: direct(직접발급), download(다운로드), auto(자동발급) (`all`=전체) | +| issue_condition | query | string | 아니오 | `all`, `manual`, `signup`, `first_purchase`, `birthday` | 발급조건 필터: manual(수동), signup(회원가입), first_purchase(첫구매), birthday(생일) (`all`=전체) | +| min_benefit_amount | query | number | 아니오 | min 0 | 혜택값(할인 금액/율) 하한 필터 | +| max_benefit_amount | query | number | 아니오 | min 0 | 혜택값(할인 금액/율) 상한 필터 | +| min_order_amount | query | number | 아니오 | min 0 | 최소 주문금액 하한 필터 | +| created_start_date | query | date | 아니오 | — | 생성일시 범위 시작 | +| created_end_date | query | date | 아니오 | — | 생성일시 범위 종료 (시작일 이후) | +| valid_start_date | query | date | 아니오 | — | 유효기간 범위 시작 | +| valid_end_date | query | date | 아니오 | — | 유효기간 범위 종료 (시작일 이후) | +| issue_start_date | query | date | 아니오 | — | 발급기간 범위 시작 | +| issue_end_date | query | date | 아니오 | — | 발급기간 범위 종료 (시작일 이후) | +| created_by | query | uuid | 아니오 | — | 등록자(생성한 관리자) UUID 필터 | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `157` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"API 문서 샘플 쿠폰","en":"API Doc Sample Coupon"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `API 문서 샘플 쿠폰` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| description | object | `{"ko":null,"en":null}` | 설명 (다국어 필드는 로케일별 값 객체) | +| localized_description | string | `설날 특별 무료배송 예정` | `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 | integer | `2000` | 최대 할인액 (정률 할인 시 상한 금액, 미설정 시 null) | +| min_order_amount | integer | `0` | 쿠폰 적용 최소 주문금액 (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 | object | `{"KRW":{"price":10000,"formatted":"10,000원","is_default":…` | 최소 주문금액의 통화별 환산 맵 (0이면 null) | +| multi_currency_discount_max_amount | object | `{"KRW":{"price":2000,"formatted":"2,000원","is_default":tr…` | 최대 할인액의 통화별 환산 맵 (미설정 시 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 | integer | `1` | 총 발급 수량 (null=무제한) | +| issued_count | integer | `0` | issued 개수 (집계) | +| per_user_limit | integer | `1` | 회원 1인당 발급 제한 수량 (0=무제한) | +| issue_count_formatted | string | `0/무제한` | `issue_count` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| valid_type | string | `period` | 유효기간 유형: period(기간 지정), days_from_issue(발급일로부터 N일) | +| valid_days | integer | `1` | 발급일로부터 유효 일수 (valid_type=days_from_issue 일 때) | +| valid_from | string | `2026-06-30` | 유효기간 시작일 (사이트 타임존 기준 날짜 문자열) | +| valid_to | string | `2026-07-30` | 유효기간 종료일 (사이트 타임존 기준 날짜 문자열) | +| valid_period_formatted | string | `-` | `valid_period` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | +| issue_from | string | `2026-06-30T11:24` | 발급기간 시작 일시 (datetime-local 입력 호환 문자열) | +| issue_to | string | `2026-07-15T11:24` | 발급기간 종료 일시 (datetime-local 입력 호환 문자열) | +| 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-07 14:47:31` | 생성 일시 | +| updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | +| created_by | string | `a1e0a91a-fba6-491c-a53e-7285a5686857` | 등록자(생성한 관리자) UUID (creator 관계 로드 시) | +| created_by_name | string | `-` | 등록자 이름 (creator 미로드/미설정 시 `-`) | +| created_by_email | string | `heuristing@gmail.com` | 등록자 이메일 (creator 관계 파생) | +| creator | object | `{"uuid":"a1e0a91a-fba6-491c-a53e-7285a5686857","name":"관리자"}` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| issues_count | integer | `0` | issues 개수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 프로모션 쿠폰(쿠폰 정의/마스터) 목록을 검색·필터·정렬·페이지네이션으로 조회합니다. `permission:sirsoft-ecommerce.promotion-coupon.read` 권한이 필요하며, 이름/설명/생성자 검색, 적용대상·혜택유형·발급상태·발급방법·발급조건 필터, 혜택금액·주문금액·생성/유효/발급 기간 범위 필터를 지원합니다. `CouponService::getCoupons()`가 조회하고 `CouponCollection`으로 직렬화하며, 각 항목은 다국어 라벨·배지 색상·다중통화 혜택값 등 관리자 UI 표시용 파생 필드를 포함합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/promotion-coupons + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.promotion-coupons.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CouponController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.promotion-coupon.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| description | body | array | 아니오 | — | 설명 | +| target_type | body | string | 예 | `product_amount`, `order_amount`, `shipping_fee` | 적용대상(할인 기준): 상품금액/주문금액/배송비 | +| discount_type | body | string | 예 | `fixed`, `rate` | 혜택유형: fixed(정액 금액), rate(정률 %) | +| discount_value | body | number | 예 | min 1 | 혜택값 (정액이면 할인 금액, 정률이면 1~100 할인율 %) | +| discount_max_amount | body | number | 아니오 | min 0 | 최대 할인액 (정률 할인 시 상한 금액) | +| min_order_amount | body | number | 아니오 | min 0 | 쿠폰 적용 최소 주문금액 (미입력 시 0=제한 없음) | +| issue_method | body | string | 예 | `direct`, `download`, `auto` | 발급방법: direct(직접발급), download(다운로드), auto(자동발급) | +| issue_condition | body | string | 예 | `manual`, `signup`, `first_purchase`, `birthday` | 발급조건: manual(수동), signup(회원가입), first_purchase(첫구매), birthday(생일) | +| issue_status | body | string | 예 | `issuing`, `stopped` | 발급상태: issuing(발급중), stopped(발급중단) | +| total_quantity | body | integer | 아니오 | min 1 | 총 발급 수량 (미입력 시 무제한) | +| per_user_limit | body | integer | 예 | min 0 | 회원 1인당 발급 제한 수량 (0=무제한) | +| valid_type | body | string | 예 | `period`, `days_from_issue` | 유효기간 유형: period(기간 지정, valid_from/valid_to 필수), days_from_issue(발급일로부터 N일, valid_days 필수) | +| valid_days | body | integer | 아니오 | min 1 | 발급일로부터 유효 일수 (valid_type=days_from_issue 시 필수) | +| valid_from | body | date | 아니오 | — | 유효기간 시작일 (valid_type=period 시 필수) | +| valid_to | body | date | 아니오 | — | 유효기간 종료일 (valid_type=period 시 필수, valid_from 이후) | +| issue_from | body | date | 아니오 | — | 발급기간 시작 일시 (미입력 시 상시발급) | +| issue_to | body | date | 아니오 | — | 발급기간 종료 일시 (issue_from 이후) | +| is_combinable | body | boolean | 아니오 | — | combinable 여부 | +| target_scope | body | string | 아니오 | `all`, `products`, `categories` | 적용 범위: all(전체 상품), products(특정 상품), categories(특정 카테고리) | +| products | body | array | 아니오 | — | 적용 상품 목록 (`target_scope=products`), 항목별 `{id, type: include\|exclude}` | +| categories | body | array | 아니오 | — | 적용 카테고리 목록 (`target_scope=categories`), 항목별 `{id, type: include\|exclude}` | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.coupon.create_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 새 프로모션 쿠폰(정의)을 생성합니다. `permission:sirsoft-ecommerce.promotion-coupon.create` 권한이 필요하며, 다국어 쿠폰명(`name`), 적용대상·혜택유형·혜택값, 발급방법/조건/상태, 유효기간·발급기간, 회원당 한도, 적용 범위(`target_scope`: all·products·categories)와 그에 따른 상품/카테고리 배열을 받아 `CouponService::createCoupon()`이 저장하고 생성된 쿠폰을 201로 반환합니다. `target_scope` 가 products/categories 일 때만 각 배열이 의미를 가지며, 확장은 `sirsoft-ecommerce.coupon.create_validation_rules` 필터로 검증 규칙을 추가할 수 있습니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/promotion-coupons/bulk-status + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.promotion-coupons.bulk-status` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CouponController@bulkUpdateStatus` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.promotion-coupon.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| issue_status | body | string | 예 | `issuing`, `stopped` | 일괄 적용할 발급상태: issuing(발급중), stopped(발급중단) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 여러 쿠폰의 발급상태(`issue_status`: issuing·stopped)를 한 번에 변경합니다. `permission:sirsoft-ecommerce.promotion-coupon.update` 권한이 필요하고 `ids` 로 대상 쿠폰들을 지정하며, `CouponService::bulkUpdateIssueStatus()`가 일괄 갱신 후 변경된 건수(`updated_count`)를 반환합니다. 쿠폰 목록에서 여러 항목을 선택해 발급을 일괄 중단/재개할 때 사용합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/promotion-coupons/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.promotion-coupons.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CouponController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.promotion-coupon.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 프로모션 쿠폰(정의) 1건을 삭제합니다. `permission:sirsoft-ecommerce.promotion-coupon.delete` 권한이 필요하며, `CouponService::deleteCoupon()`이 삭제를 수행합니다(쿠폰 모델은 소프트 삭제 대상). 이미 발급된 내역이 있는 등 도메인 제약으로 삭제가 실패하면 400 오류로 응답합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/promotion-coupons/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.promotion-coupons.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CouponController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.promotion-coupon.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 프로모션 쿠폰 1건의 상세 정보를 조회합니다. `permission:sirsoft-ecommerce.promotion-coupon.read` 권한이 필요하며, `CouponService::getCoupon()`이 쿠폰 정의와 함께 적용 범위(included/excluded products·categories)까지 로드해 `CouponResource`로 반환합니다. 목록(index)보다 상세한 필드(적용 대상 상품/카테고리 목록 등)를 포함하며, 해당 쿠폰이 없으면 404 를 반환합니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/promotion-coupons/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.promotion-coupons.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CouponController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.promotion-coupon.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| description | body | array | 아니오 | — | 설명 | +| target_type | body | string | 예 | `product_amount`, `order_amount`, `shipping_fee` | 적용대상(할인 기준): 상품금액/주문금액/배송비 | +| discount_type | body | string | 예 | `fixed`, `rate` | 혜택유형: fixed(정액 금액), rate(정률 %) | +| discount_value | body | number | 예 | min 1 | 혜택값 (정액이면 할인 금액, 정률이면 1~100 할인율 %) | +| discount_max_amount | body | number | 아니오 | min 0 | 최대 할인액 (정률 할인 시 상한 금액) | +| min_order_amount | body | number | 아니오 | min 0 | 쿠폰 적용 최소 주문금액 (미입력 시 0=제한 없음) | +| issue_method | body | string | 예 | `direct`, `download`, `auto` | 발급방법: direct(직접발급), download(다운로드), auto(자동발급) | +| issue_condition | body | string | 예 | `manual`, `signup`, `first_purchase`, `birthday` | 발급조건: manual(수동), signup(회원가입), first_purchase(첫구매), birthday(생일) | +| issue_status | body | string | 예 | `issuing`, `stopped` | 발급상태: issuing(발급중), stopped(발급중단) | +| total_quantity | body | integer | 아니오 | min 1 | 총 발급 수량 (미입력 시 무제한) | +| per_user_limit | body | integer | 예 | min 0 | 회원 1인당 발급 제한 수량 (0=무제한) | +| valid_type | body | string | 예 | `period`, `days_from_issue` | 유효기간 유형: period(기간 지정, valid_from/valid_to 필수), days_from_issue(발급일로부터 N일, valid_days 필수) | +| valid_days | body | integer | 아니오 | min 1 | 발급일로부터 유효 일수 (valid_type=days_from_issue 시 필수) | +| valid_from | body | date | 아니오 | — | 유효기간 시작일 (valid_type=period 시 필수) | +| valid_to | body | date | 아니오 | — | 유효기간 종료일 (valid_type=period 시 필수, valid_from 이후) | +| issue_from | body | date | 아니오 | — | 발급기간 시작 일시 (미입력 시 상시발급) | +| issue_to | body | date | 아니오 | — | 발급기간 종료 일시 (issue_from 이후) | +| is_combinable | body | boolean | 아니오 | — | combinable 여부 | +| target_scope | body | string | 아니오 | `all`, `products`, `categories` | 적용 범위: all(전체 상품), products(특정 상품), categories(특정 카테고리) | +| products | body | array | 아니오 | — | 적용 상품 목록 (`target_scope=products`), 항목별 `{id, type: include\|exclude}` | +| categories | body | array | 아니오 | — | 적용 카테고리 목록 (`target_scope=categories`), 항목별 `{id, type: include\|exclude}` | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.coupon.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 기존 프로모션 쿠폰(정의)의 내용을 수정합니다. `permission:sirsoft-ecommerce.promotion-coupon.update` 권한이 필요하며, 생성과 동일한 필드 집합(쿠폰명·혜택·발급 조건·유효/발급 기간·적용 범위 등)을 받아 `CouponService::updateCoupon()`이 전체 갱신합니다. `target_scope` 변경 시 그에 맞는 상품/카테고리 배열을 함께 보내야 하며, 대상 쿠폰이 없으면 404, 갱신 실패 시 400 을 반환합니다. 확장은 `sirsoft-ecommerce.coupon.update_validation_rules` 필터로 검증 규칙을 추가할 수 있습니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/promotion-coupons/{id}/issue-direct + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.promotion-coupons.issue-direct` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CouponController@issueDirect` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.promotion-coupon.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| user_uuids | body | array | 예 | min 1 | 쿠폰을 직접 발급할 대상 회원 UUID 배열 (내부 회원 ID 로 해석 후 발급) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 지정한 회원들에게 특정 쿠폰을 즉시 직접 발급합니다. `permission:sirsoft-ecommerce.promotion-coupon.update` 권한이 필요하고 대상 회원은 `user_uuids`(uuid 배열)로 지정하며, FormRequest 가 uuid 를 내부 회원 ID 로 해석한 뒤 `CouponService::issueDirectly()`가 발급합니다. 응답에는 실제 발급 건수(`issued`)와 이미 보유/한도 초과 등으로 건너뛴 목록(`skipped`)이 포함되고, 건너뛴 건이 있으면 별도 안내 메시지 키가 사용됩니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/promotion-coupons/{id}/issues + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.promotion-coupons.issues` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CouponController@issues` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.promotion-coupon.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| user_id | query | uuid | 아니오 | — | user 식별자 | +| status | query | string | 아니오 | `available`, `used`, `expired`, `cancelled` | 상태 필터 (해당 상태의 항목만 조회) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.coupon.issues_list_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 특정 쿠폰의 회원별 발급 내역을 회원(`user_id`)·상태(available·used·expired·cancelled)로 필터링해 페이지네이션으로 조회합니다. `permission:sirsoft-ecommerce.promotion-coupon.read` 권한이 필요하며, `CouponService::getCouponIssues()`가 조회하고 `CouponIssueCollection`으로 직렬화합니다. 어떤 회원이 이 쿠폰을 받아 언제 사용/만료/취소했는지 추적하는 발급 원장 화면에 사용됩니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/promotion-coupons/{id}/issues/{issueId} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.promotion-coupons.issues.cancel` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\CouponController@cancelIssue` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.promotion-coupon.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| issueId | path | string | 예 | — | 대상 issue의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 특정 쿠폰의 발급 내역 1건을 취소 처리합니다. 대상은 쿠폰 ID(`id`)와 발급 내역 ID(`issueId`) 조합으로 지정하고 `permission:sirsoft-ecommerce.promotion-coupon.update` 권한이 필요하며, `CouponService::cancelIssue()`가 미사용 발급 건만 취소합니다. 이미 사용된 발급 건 등 취소 불가 사유는 예외 메시지(`detail`)로 관리자에게 그대로 노출되어 400 으로 응답합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md b/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md new file mode 100644 index 00000000..5d32222e --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md @@ -0,0 +1,46 @@ +# Review Image API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Review Image 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/review-image/{hash} + +- **라우트명**: `api.modules.sirsoft-ecommerce.review-image.download` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\ReviewImageController@download` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 리뷰에 첨부된 이미지를 해시(12자) 기반으로 공개 서빙합니다. 인증이 필요 없으며, `ReviewImageController@download` 가 `ProductReviewImageService::download()` 로 해시에 해당하는 이미지를 찾아 스트림(`StreamedResponse`)으로 반환합니다. 해시에 해당하는 이미지가 없으면 404 를 반환합니다. `` 등에서 리뷰 이미지 원본을 표시할 때 사용하며, 실제 파일 경로를 노출하지 않고 해시로만 접근하게 합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md b/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md new file mode 100644 index 00000000..e5b41f8e --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md @@ -0,0 +1,467 @@ +# Reviews API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Reviews 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/reviews + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.reviews.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductReviewController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.reviews.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| search_field | query | string | 아니오 | `all`, `product_name`, `reviewer`, `content`, `order_number`, `option_name` | 검색 대상 필드명 (검색어를 적용할 컬럼) | +| search_keyword | query | string | 아니오 | max 200 | 검색 키워드 (부분 일치) | +| rating | query | string | 아니오 | `1`, `2`, `3`, `4`, `5`, `` | 별점 필터 (해당 별점의 리뷰만 조회, 빈 값은 전체) | +| reply_status | query | string | 아니오 | `all`, `replied`, `unreplied` | 답변 상태 필터 (답변완료/미답변) | +| photo | query | string | 아니오 | `photo`, `normal`, `` | 포토 리뷰 필터 (이미지 첨부 여부, 빈 값은 전체) | +| has_photo | query | boolean | 아니오 | — | photo 여부 | +| status | query | string | 아니오 | — | 상태 필터 (해당 상태의 항목만 조회) | +| start_date | query | date | 아니오 | — | 조회 기간 시작일 (이 날짜 이후 데이터) | +| end_date | query | date | 아니오 | — | 조회 기간 종료일 (이 날짜 이전 데이터) | +| sort | query | string | 아니오 | `created_at_desc`, `created_at_asc`, `rating_desc`, `rating_asc` | 정렬 기준 (필드명, `-` 접두 시 내림차순) | +| sort_by | query | string | 아니오 | `created_at`, `rating`, `reply_status` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| per_page | query | integer | 아니오 | min 10, max 100 | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.review.list_validation_rules`, `sirsoft-ecommerce.review.list_validation_messages`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `99` | 기본 키 (내부 식별자) | +| product_id | integer | `320` | product 식별자 (연관 리소스 참조) | +| order_option_id | integer | `859` | order option 식별자 (연관 리소스 참조) | +| user_id | string | `a231747f-e82e-4cf2-9ae1-a261849dce40` | user 식별자 (연관 리소스 참조) | +| user | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 작성자 정보 (uuid·name·email, `user` 관계 로드 시) | +| product | object | `{"id":320,"name":"API 문서 샘플 상품","thumbnail_url":null}` | 리뷰 대상 상품 정보 (id·현지화 상품명·썸네일 URL) | +| option_snapshot | null | `null` | 주문 시점 옵션 스냅샷 (옵션명 보존용) | +| option_snapshot_label | string | `` | `option_snapshot` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| rating | integer | `5` | 별점 (1~5) | +| content | string | `Molestiae repellendus accusantium omn…` | 리뷰 내용 | +| content_mode | string | `text` | 콘텐츠 모드: text / html | +| status | string | `visible` | 리뷰 상태: visible(전시중) / hidden(숨김) | +| status_label | string | `전시중` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| status_badge_color | string | `blue` | 상태 뱃지 색상 (visible=blue / hidden=gray) | +| images | array | `[]` | 첨부 이미지 목록 (이미지 리소스 배열, `images` 관계 로드 시) | +| image_count | integer | `0` | image 개수 (집계) | +| orderOption | object | `{"id":859,"order_id":455,"order_number":"ORD-20260707-000…` | 리뷰가 연결된 주문 옵션 정보 (주문 ID·주문번호·수량·주문일) | +| has_reply | boolean | `false` | reply 여부 | +| has_reply_label | string | `미답변` | `has_reply` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| has_reply_badge_color | string | `gray` | 답변 여부 뱃지 색상 (답변완료=green / 미답변=gray) | +| reply_content | null | `null` | 판매자 답변 내용 (없으면 null) | +| reply_content_mode | string | `text` | 답변 콘텐츠 모드: text / html | +| reply_admin_uuid | null | `null` | 답변 작성 관리자 UUID (`replyAdmin` 관계 로드 시) | +| reply_admin | null | `null` | 답변 작성 관리자 정보 (uuid·name·email, `replyAdmin` 관계 로드 시) | +| replied_at | null | `null` | replied 일시 | +| reply_updated_at | null | `null` | reply updated 일시 | +| created_at | string | `2026-07-07 14:47:31` | 생성 일시 | +| updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | +| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.reviews.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 전체 상품 리뷰를 페이지네이션으로 조회합니다. `sirsoft-ecommerce.reviews.read` 권한이 필요하며, `ProductReviewService::getAdminList()`가 검색어·별점·답변 여부·포토 여부·상태·기간 등 필터와 정렬을 적용해 목록을 반환합니다. 각 항목에는 작성자·상품·주문옵션·이미지·답변 정보가 함께 로드되고, `abilities` 로 현재 관리자의 수정/삭제 가능 여부가 내려옵니다. 리뷰 관리 화면의 목록 표를 채우는 데 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/reviews/bulk + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.reviews.bulk` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductReviewController@bulk` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.reviews.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| action | body | string | 예 | `delete`, `change_status` | 일괄 작업 종류 (delete=삭제, change_status=상태 변경) | +| status | body | string | 아니오 | — | 변경할 리뷰 상태 (visible/hidden, `action=change_status` 시 필수) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.review.bulk_validation_rules`, `sirsoft-ecommerce.review.bulk_validation_messages`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.reviews.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 선택한 여러 리뷰를 한 번에 일괄 처리합니다. `sirsoft-ecommerce.reviews.update` 권한이 필요하며, `action` 이 `delete` 이면 `ProductReviewService::bulkDelete()` 로 삭제하고 `deleted_count` 를, `change_status` 이면 `bulkUpdateStatus()` 로 `status` 값으로 상태를 변경하고 `updated_count` 를 반환합니다. `change_status` 를 선택했다면 `status` 값이 반드시 필요합니다. 목록 화면에서 체크박스로 다건 선택 후 삭제/전시상태 변경 시 사용합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/reviews/{review} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.reviews.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductReviewController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.reviews.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| review | path | string | 예 | — | 대상 review의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.reviews.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 리뷰 1건을 삭제합니다. `sirsoft-ecommerce.reviews.delete` 권한이 필요하며, 삭제 전 `images` 관계를 로드한 뒤 `ProductReviewService::deleteReview()` 가 첨부 이미지 파일까지 함께 정리하며 리뷰를 제거합니다. 라우트 모델 바인딩으로 존재하지 않는 리뷰는 404 를 반환합니다. 부적절한 리뷰를 관리자 화면에서 개별 삭제할 때 사용합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/reviews/{review} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.reviews.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductReviewController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.reviews.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| review | path | string | 예 | — | 대상 review의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `99` | 기본 키 (내부 식별자) | +| product_id | integer | `320` | 상품 ID | +| order_option_id | integer | `859` | 주문 옵션 ID | +| user_id | string | `a231747f-e82e-4cf2-9ae1-a261849dce40` | 작성자 ID | +| user | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 작성자 정보 (uuid·name·email, `user` 관계 로드 시) | +| product | object | `{"id":320,"name":"API 문서 샘플 상품","thumbnail_url":null}` | 리뷰 대상 상품 정보 (id·현지화 상품명·썸네일 URL) | +| option_snapshot | null | `null` | 주문 시점 옵션 스냅샷 (옵션명 보존용) | +| option_snapshot_label | string | `` | `option_snapshot` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| rating | integer | `5` | 별점 (1~5) | +| content | string | `Molestiae repellendus accusantium omn…` | 리뷰 내용 | +| content_mode | string | `text` | 콘텐츠 모드: text / html | +| status | string | `visible` | 리뷰 상태: visible / hidden | +| status_label | string | `전시중` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) | +| status_badge_color | string | `blue` | 상태 뱃지 색상 (visible=blue / hidden=gray) | +| images | array | `[]` | 첨부 이미지 목록 (이미지 리소스 배열, `images` 관계 로드 시) | +| image_count | integer | `0` | image 개수 (집계) | +| orderOption | object | `{"id":859,"order_id":455,"order_number":"ORD-20260707-000…` | 리뷰가 연결된 주문 옵션 정보 (주문 ID·주문번호·수량·주문일) | +| 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 | +| reply_admin_uuid | null | `null` | 답변 작성 관리자 UUID (`replyAdmin` 관계 로드 시) | +| reply_admin | null | `null` | 답변 작성 관리자 정보 (uuid·name·email, `replyAdmin` 관계 로드 시) | +| replied_at | null | `null` | replied 일시 | +| reply_updated_at | null | `null` | reply updated 일시 | +| created_at | string | `2026-07-07 14:47:31` | 생성 일시 | +| updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | +| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.reviews.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 리뷰 1건의 상세 정보를 조회합니다. `sirsoft-ecommerce.reviews.read` 권한이 필요하며, 컨트롤러가 `user`·`product`·`images`·`replyAdmin`·`orderOption.order` 관계를 함께 로드해 작성자·상품·이미지·판매자 답변·주문 정보까지 포함한 단건 리소스를 반환합니다. 관리자 리뷰 상세/답변 작성 화면 진입 시 사용합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/reviews/{review}/reply + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.reviews.reply.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductReviewController@destroyReply` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.reviews.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| review | path | string | 예 | — | 대상 review의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.reviews.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 리뷰에 등록한 판매자 답변을 삭제합니다. `sirsoft-ecommerce.reviews.update` 권한이 필요하며, `ProductReviewService::deleteReply()` 가 답변 내용·작성자·작성 일시를 비우고 답변이 제거된 리뷰 리소스를 반환합니다. 잘못 작성한 답변을 회수할 때 사용하며, 리뷰 자체는 유지됩니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/reviews/{review}/reply + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.reviews.reply.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductReviewController@storeReply` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.reviews.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| review | path | string | 예 | — | 대상 review의 식별자 | +| reply_content | body | string | 예 | min 1, max 2000 | 판매자 답변 내용 (1~2000자) | +| reply_content_mode | body | string | 아니오 | `text`, `html` | 답변 콘텐츠 모드 (평문/HTML, 미지정 시 text) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.review.store_reply_validation_rules`, `sirsoft-ecommerce.review.store_reply_validation_messages`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.reviews.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 리뷰에 판매자 답변을 등록하거나 기존 답변을 수정합니다. `sirsoft-ecommerce.reviews.update` 권한이 필요하며, `ProductReviewService::saveReply()` 가 로그인 관리자 UUID(`Auth::id()`)를 답변 작성자로 기록하고 `reply_content`(1~2000자)와 `reply_content_mode`(text/html)를 저장합니다. 답변이 이미 있으면 갱신되고 작성 일시가 채워집니다. 고객 리뷰에 판매자가 응대할 때 사용합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/reviews/{review}/status + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.reviews.update-status` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ProductReviewController@updateStatus` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.reviews.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| review | path | string | 예 | — | 대상 review의 식별자 | +| status | body | string | 예 | — | 변경할 리뷰 전시 상태 (visible=전시중 / hidden=숨김) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.review.update_status_validation_rules`, `sirsoft-ecommerce.review.update_status_validation_messages`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.reviews.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 리뷰의 전시 상태를 변경합니다. `sirsoft-ecommerce.reviews.update` 권한이 필요하며, `ProductReviewService::updateStatus()` 가 `status` 값(예: visible/hidden)으로 리뷰를 전시하거나 숨기고 갱신된 리뷰 리소스를 반환합니다. 신고되었거나 부적절한 리뷰를 노출에서 제외하거나 다시 노출할 때 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/user/reviews + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.reviews.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\ProductReviewController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.user-reviews.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| product_id | body | integer | 예 | — | product 식별자 | +| order_option_id | body | integer | 예 | — | order option 식별자 | +| rating | body | integer | 예 | min 1, max 5 | 별점 (1~5) | +| content | body | string | 예 | min 10, max 2000 | 리뷰 내용 (10~2000자) | +| content_mode | body | string | 아니오 | `text`, `html` | 콘텐츠 모드 (평문/HTML, 미지정 시 text) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.review.store_validation_rules`, `sirsoft-ecommerce.review.store_validation_messages`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-reviews.write`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 로그인 회원이 구매한 상품에 리뷰를 작성합니다. `sirsoft-ecommerce.user-reviews.write` 권한이 필요하며, `ProductReviewService::createReview()` 가 로그인 사용자(`Auth::id()`)를 작성자로 하여 `product_id`·`order_option_id`·별점(1~5)·내용(10~2000자)으로 리뷰를 생성하고 201 로 반환합니다. 본인 주문이 아니거나 이미 작성했거나 작성 조건을 만족하지 못하면 서비스가 `RuntimeException` 을 던져 422 로 응답합니다. 마이페이지 리뷰 작성 폼에서 사용합니다. + + +### GET /api/modules/sirsoft-ecommerce/user/reviews/can-write/{orderOptionId} + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.reviews.can-write` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\ProductReviewController@canWrite` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderOptionId | path | string | 예 | — | 대상 order option의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 로그인 회원이 특정 주문 옵션에 대해 리뷰를 쓸 수 있는지 확인합니다. `auth:sanctum` 인증만 요구하며, `ProductReviewService::canWrite()` 가 본인 주문 여부·구매 완료·중복 작성 여부 등을 판정해 `can_write` 불리언과 불가 시 `reason`(예: `not_own_order`)을 반환합니다. 리뷰 작성 버튼 노출 여부를 결정하기 위해 상품/주문 화면에서 사전 호출합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/user/reviews/{review} + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.reviews.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\ProductReviewController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.user-reviews.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| review | path | string | 예 | — | 대상 review의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-reviews.write`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 로그인 회원이 본인이 작성한 리뷰를 삭제합니다. `sirsoft-ecommerce.user-reviews.write` 권한이 필요하며, 컨트롤러가 `review->user_id` 와 로그인 사용자를 대조해 본인 소유가 아니면 403 을 반환합니다. 본인 리뷰이면 `images` 관계를 로드한 뒤 `ProductReviewService::deleteReview()` 가 첨부 이미지까지 함께 삭제합니다. 마이페이지에서 자신의 리뷰를 지울 때 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/user/reviews/{review}/images + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.reviews.images.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\ReviewImageController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.user-reviews.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| review | path | string | 예 | — | 대상 review의 식별자 | +| image | body | file | 예 | max 10240 | 첨부할 이미지 파일 (최대 용량은 리뷰 설정 `review_settings.max_image_size_mb` 기반, 폴백 10MB) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-reviews.write`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 로그인 회원이 자신의 리뷰에 이미지를 첨부합니다. `sirsoft-ecommerce.user-reviews.write` 권한이 필요하며, 컨트롤러가 `review->user_id` 로 본인 소유를 확인(불일치 시 403)한 뒤 `ProductReviewImageService::upload()` 가 업로드된 이미지(최대 10MB)를 저장하고 201 로 이미지 리소스를 반환합니다. 파일 형식/크기 등 제약 위반 시 서비스가 `RuntimeException` 을 던져 422 로 응답합니다. 포토 리뷰 작성 시 이미지를 추가할 때 사용합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/user/reviews/{review}/images/{image} + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.reviews.images.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\ReviewImageController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.user-reviews.write` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| review | path | string | 예 | — | 대상 review의 식별자 | +| image | path | string | 예 | — | 대상 image의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-reviews.write`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 로그인 회원이 자신의 리뷰에서 첨부 이미지 1건을 삭제합니다. `sirsoft-ecommerce.user-reviews.write` 권한이 필요하며, 컨트롤러가 `review->user_id` 로 본인 소유를 확인(불일치 시 403)하고 `image->review_id` 가 해당 리뷰에 속하는지 대조(불일치 시 404)한 뒤 `ProductReviewImageService::delete()` 로 파일과 레코드를 함께 제거합니다. 포토 리뷰에서 잘못 올린 이미지를 개별 삭제할 때 사용합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/settings.md b/modules/_bundled/sirsoft-ecommerce/docs/api/settings.md new file mode 100644 index 00000000..0b35ab25 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/settings.md @@ -0,0 +1,324 @@ +# Settings API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Settings 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/settings + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.settings.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\EcommerceSettingsController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`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 | `[{"id":"kginicis","name_key":"sirsoft-pay_kginicis::provi…` | 설치된 PG 플러그인이 훅으로 등록한 PG 제공자 목록 (id·name_key·지원 결제수단) | +| abilities | object | `{"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.read`)이 없는 경우 | + + + +**설명** 관리자가 이커머스 모듈의 전체 환경설정을 카테고리별로 묶어 한 번에 조회합니다. `permission:sirsoft-ecommerce.settings.read` 권한이 필요하며, `EcommerceSettingsService::getAllSettings()`로 JSON 설정을 읽은 뒤 DB 관리 대상(배송사·배송유형·클레임 사유·마일리지 알림 채널)과 등록된 PG 목록을 병합해 반환합니다. `basic_info`·`shipping`·`order_settings`·`claim`·`mileage` 등 관리자 설정 화면 전 탭의 초기 데이터를 이 한 응답으로 채웁니다. 응답의 `abilities.can_update` 로 수정 권한 보유 여부도 함께 내려갑니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/settings + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.settings.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\EcommerceSettingsController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| _tab | body | string | 아니오 | `basic_info`, `language_currency`, `seo`, `order_settings`, `claim`, `shipping`, `review_settings`, `notification_definitions`, `notifications`, `inquiry`, `mileage` | 저장할 설정 탭(카테고리) 지정 (탭별 부분 저장 식별용) | +| notifications | body | array | 아니오 | — | 알림 채널 설정 배열 (채널 ID·활성 여부·정렬 순서) | +| basic_info | body | array | 아니오 | — | 쇼핑몰 기본 정보 섹션 (쇼핑몰명·라우트 경로·상호·사업자번호·주소·연락처 등) | +| language_currency | body | array | 아니오 | — | 통화 설정 섹션 (기본 통화·통화 목록: 코드·다국어명·환율·반올림 규칙·통화별 로케일) | +| seo | body | array | 아니오 | — | SEO 메타 설정 섹션 (페이지 유형별 메타 타이틀/설명·SEO 활성 토글) | +| inquiry | body | array | 아니오 | — | 문의 연동 설정 섹션 (문의 게시판 slug) | +| order_settings | body | array | 아니오 | — | 주문/결제 설정 섹션 (기본 PG·결제수단·은행/무통장 계좌·자동취소·장바구니 만료 등) | +| claim | body | array | 아니오 | — | 클레임 설정 섹션 (환불 사유 목록, DB 동기화 대상으로 분리 저장) | +| review_settings | body | array | 아니오 | — | 리뷰 정책 섹션 (작성 기한일·이미지 최대 개수·이미지 최대 용량 MB) | +| mileage | body | array | 아니오 | — | 마일리지 설정 섹션 (사용 여부·기본 적립률·적립 트리거·통화별 규칙·소멸/소멸 알림) | +| shipping | body | array | 아니오 | — | 배송 설정 섹션 (기본 국가·배송 가능 국가·무료배송·배송사(carriers)·배송유형(types) — carriers/types는 DB 동기화 대상으로 분리 저장) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 이커머스 환경설정을 저장합니다. `permission:sirsoft-ecommerce.settings.update` 권한이 필요하며, `_tab` 으로 저장할 카테고리를 지정하고 각 섹션(`basic_info`·`shipping`·`claim` 등)을 배열로 전달합니다. `EcommerceSettingsService::saveSettings()`가 JSON 설정을 저장하되, DB 관리 대상인 `shipping.carriers`·`shipping.types`·`claim.refund_reasons` 는 분리해 각 Service 의 sync 메서드로 동기화합니다. 저장 성공 시 `sirsoft-ecommerce.settings.after_save` 훅을 발화하고, 관리자 UI 상태 갱신을 위해 병합된 전체 설정을 다시 반환합니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/settings/banks + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.settings.store-banks` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\EcommerceSettingsController@storeBanks` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| banks | body | array | 아니오 | — | 무통장입금용 은행 목록 (은행 코드·다국어 은행명) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 무통장입금용 은행 목록만 별도로 저장합니다. `permission:sirsoft-ecommerce.settings.update` 권한이 필요하며, `banks` 배열을 받아 `EcommerceSettingsService::saveBanks()`가 저장합니다. 전체 설정 저장(`store`)과 분리된 전용 엔드포인트로, 결제 설정 화면에서 은행 목록만 관리할 때 사용합니다. 저장 성공 시 갱신된 전체 설정을 반환합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/settings/clear-cache + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.settings.clear-cache` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\EcommerceSettingsController@clearCache` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.update` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | + + + +**설명** 관리자가 이커머스 설정 캐시와 SEO 렌더 캐시를 초기화합니다. `permission:sirsoft-ecommerce.settings.update` 권한이 필요하며, `EcommerceSettingsService::clearCache()`로 설정 캐시를 비우고 `SeoCacheManagerInterface::clearAll()`로 SEO 페이지 캐시까지 전부 삭제합니다. 설정 변경이 화면에 즉시 반영되지 않을 때 캐시를 강제로 비우는 용도로, 성공 시 `{cleared: true}` 를 반환합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/settings/seo-cache-info + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.settings.seo-cache-info` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\EcommerceSettingsController@seoCacheInfo` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| count | integer | `0` | 캐시된 SEO 페이지 URL 개수 | +| size_bytes | integer | `0` | 캐시된 SEO 페이지의 지원 로케일별 HTML 총 바이트 | +| size_formatted | string | `0 B` | `size` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.read`)이 없는 경우 | + + + +**설명** 관리자가 현재 캐시된 SEO 페이지의 개수와 총 용량을 조회합니다. `permission:sirsoft-ecommerce.settings.read` 권한이 필요하며, `SeoCacheManagerInterface::getCachedUrls()`로 캐시된 URL 을 열거하고 지원 로케일별 HTML 바이트를 합산합니다. 응답은 캐시 페이지 수(`count`)·총 바이트(`size_bytes`)·사람이 읽기 쉬운 크기(`size_formatted`, 예 `1.5 MB`)를 담습니다. 설정 화면에서 SEO 캐시 현황을 표시하고 캐시 초기화 여부를 판단하는 근거로 사용합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/settings/{category} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.settings.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\EcommerceSettingsController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| category | path | string | 예 | — | 분류 필터 (해당 분류의 항목만 조회) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 단일 설정 카테고리만 골라 조회합니다. `permission:sirsoft-ecommerce.settings.read` 권한이 필요하며, path 의 `category`(예 `basic_info`)로 `EcommerceSettingsService::getSettings()`를 호출해 해당 섹션만 반환합니다. 전체 설정을 내려받는 index 와 달리 특정 탭 데이터만 필요할 때 사용하며, 응답은 `category`·`settings`·`abilities.can_update` 를 포함합니다. + + +### GET /api/modules/sirsoft-ecommerce/settings/checkout + +- **라우트명**: `api.modules.sirsoft-ecommerce.settings.checkout` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\EcommerceSettingsController@checkout` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| shipping | object | `{"default_country":"KR","available_countries":[{"code":"K…` | 체크아웃용 배송 설정 (기본 국가·배송 가능 국가·무료배송·배송유형 등) | +| order_settings | object | `{"default_pg_provider":null,"payment_methods":[{"id":"car…` | 체크아웃용 주문/결제 설정 (기본 PG·활성 결제수단·무통장 계좌 등) | + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 인증 없이 접근 가능한 공개 엔드포인트로, 체크아웃 화면이 필요로 하는 배송·결제 설정을 한 번에 반환합니다. `EcommerceSettingsService::getSettings()`로 `shipping` 과 `order_settings` 두 섹션을 함께 조회하며, 개별 shipping/payment 엔드포인트를 두 번 호출하지 않도록 묶어줍니다. 비회원·회원 모두 접근하고, `logApiUsage('settings.checkout')`로 사용 로그를 남깁니다. + + +### GET /api/modules/sirsoft-ecommerce/settings/payment + +- **라우트명**: `api.modules.sirsoft-ecommerce.settings.payment` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\EcommerceSettingsController@payment` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| order_settings | object | `{"default_pg_provider":null,"payment_methods":[{"id":"car…` | 공개 가능한 결제 설정 (활성 결제수단·무통장 은행명 매핑 포함, 민감 정보 제외) | + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 인증 없이 접근 가능한 공개 엔드포인트로, 체크아웃에서 필요한 결제 설정을 반환합니다. `EcommerceSettingsService::getPublicPaymentSettings()`가 활성화된 결제 수단과 무통장입금 설정 등 공개 가능한 항목만 추려 `order_settings` 로 내려줍니다. 관리자 전용 민감 정보는 제외되며, 비회원·회원 모두 접근하고 `logApiUsage('settings.payment')`로 사용 로그를 남깁니다. + + +### GET /api/modules/sirsoft-ecommerce/settings/review + +- **라우트명**: `api.modules.sirsoft-ecommerce.settings.review` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\EcommerceSettingsController@review` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| review_settings | object | `{"write_deadline_days":90,"max_images":5,"max_image_size_…` | 공개 리뷰 정책 (작성 기한일·이미지 최대 개수·이미지 최대 용량 MB) | + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 인증 없이 접근 가능한 공개 엔드포인트로, 리뷰 작성 화면이 필요로 하는 리뷰 정책을 반환합니다. `EcommerceSettingsService::getSettings('review_settings')`로 리뷰 이미지 최대 개수(`max_images`)·최대 용량(`max_image_size_mb`)·작성 기한(`write_deadline_days`) 등을 `review_settings` 로 내려줍니다. 프론트가 이미지 업로드 제한과 작성 가능 기간을 판단하는 데 사용하며, `logApiUsage('settings.review')`로 사용 로그를 남깁니다. + + +### GET /api/modules/sirsoft-ecommerce/settings/shipping + +- **라우트명**: `api.modules.sirsoft-ecommerce.settings.shipping` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\EcommerceSettingsController@shipping` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| shipping | object | `{"default_country":"KR","available_countries":[{"code":"K…` | 공개 배송 설정 (기본 국가·배송 가능 국가·국제배송 활성 여부·배송유형·무료배송 설정) | + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 인증 없이 접근 가능한 공개 엔드포인트로, 체크아웃에서 필요한 배송 설정을 반환합니다. `EcommerceSettingsService::getSettings('shipping')`로 기본 배송 국가·이용 가능한 국가 목록·국제 배송 활성화 여부·배송 타입·무료 배송 설정 등을 `shipping` 으로 내려줍니다. 프론트가 배송지 선택과 배송비 안내를 구성하는 데 사용하며, `logApiUsage('settings.shipping')`로 사용 로그를 남깁니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-carriers.md b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-carriers.md new file mode 100644 index 00000000..889097c4 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-carriers.md @@ -0,0 +1,256 @@ +# Shipping Carriers API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Shipping Carriers 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/shipping-carriers + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-carriers.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingCarrierController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `13` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `1` | 기본 키 (내부 식별자) | +| code | string | `cj` | 배송사 고유 코드 (소문자 시작 영숫자·하이픈/언더스코어, 시스템 식별용) | +| name | object | `{"ko":"CJ대한통운","en":"CJ Logistics","ja":"CJ大韓通運"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| localized_name | string | `CJ대한통운` | `name` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) | +| type | string | `domestic` | 배송사 유형 (`domestic` 국내 / `international` 해외) | +| type_label | string | `국내` | `type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| tracking_url | string | `https://trace.cjlogistics.com/next/tr…` | tracking URL | +| is_active | boolean | `true` | active 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| created_at | string | `2026-05-27 15:20:43` | 생성 일시 | +| updated_at | string | `2026-06-27 00:49:51` | 최종 수정 일시 | +| creator | array | `[]` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| updater | array | `[]` | 최종 수정자 정보 객체 (id/name — updater 관계 로드 시) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.read`)이 없는 경우 | + + + +**설명** 관리자가 등록된 배송사 목록을 조회합니다. 배송 설정 영역이므로 `sirsoft-ecommerce.settings.read` 권한이 필요하며, `ShippingCarrierService::getAllCarriers()` 가 요청 파라미터로 목록을 조회해 `ShippingCarrierCollection` 으로 반환합니다. 각 항목에는 코드·다국어 배송사명·유형(국내/해외)·추적 URL·활성여부·정렬순서가 포함됩니다. 배송사 관리 화면의 목록을 채우는 데 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/shipping-carriers + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-carriers.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingCarrierController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| code | body | string | 예 | max 50 | 배송사 고유 코드 (소문자 시작 영숫자·하이픈/언더스코어, 중복 불가) | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| type | body | string | 예 | — | 배송사 유형 (`domestic` 국내 / `international` 해외) | +| tracking_url | body | string | 아니오 | max 500 | 배송 추적 URL 템플릿 (`{tracking_number}` 치환자를 운송장 번호로 대체) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_carrier.create_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 새 배송사를 등록합니다. `sirsoft-ecommerce.settings.update` 권한이 필요하며, `ShippingCarrierService::createCarrier()` 가 고유 코드(`code`), 다국어 배송사명(`name`), 유형(`type`, 국내/해외), 배송 추적 URL 템플릿, 활성여부, 정렬순서를 저장하고 201 로 생성된 배송사 리소스를 반환합니다. `tracking_url` 에는 `{tracking_number}` 치환자를 넣어 추적 링크를 구성합니다. 새 택배사/특송사를 시스템에 추가할 때 사용합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/shipping-carriers/active + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-carriers.active` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingCarrierController@active` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| value | integer | `1` | 배송사 ID (Select 옵션의 value) | +| label | string | `CJ대한통운` | 표시용 라벨 | +| code | string | `cj` | 배송사 고유 코드 (시스템 식별용) | +| type | string | `domestic` | 배송사 유형 (`domestic` 국내 / `international` 해외) | +| tracking_url | string | `https://trace.cjlogistics.com/next/tr…` | tracking URL | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.read`)이 없는 경우 | + + + +**설명** 활성화된 배송사만 Select 옵션 형태로 조회합니다. `sirsoft-ecommerce.settings.read` 권한이 필요하며, `ShippingCarrierService::getActiveCarriers()` 결과를 `{value, label, code, type, tracking_url}` 로 매핑해 반환합니다. `type` 쿼리(domestic/international)로 국내/해외 배송사를 필터링할 수 있습니다. 송장 등록 등에서 배송사를 선택하는 드롭다운을 채우는 데 사용하며, 비활성 배송사는 노출되지 않습니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/shipping-carriers/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-carriers.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingCarrierController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 배송사 1건을 삭제합니다. `sirsoft-ecommerce.settings.update` 권한이 필요하며, 컨트롤러가 `getCarrier()` 로 배송사를 조회해 없으면 404 를 반환한 뒤 `ShippingCarrierService::deleteCarrier()` 로 제거합니다. 주문/송장에서 사용 중이어서 삭제할 수 없는 경우 서비스 예외 메시지와 함께 400 을 반환합니다. 더 이상 쓰지 않는 배송사를 정리할 때 사용합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/shipping-carriers/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-carriers.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingCarrierController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 배송사 1건의 상세 정보를 조회합니다. `sirsoft-ecommerce.settings.read` 권한이 필요하며, `ShippingCarrierService::getCarrier()` 가 배송사를 조회해 없으면 404 를 반환하고, 있으면 코드·다국어명·유형·추적 URL·활성여부 등을 담은 단건 리소스를 반환합니다. 배송사 수정 화면 진입 시 기존 값을 불러오는 데 사용합니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/shipping-carriers/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-carriers.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingCarrierController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| code | body | string | 아니오 | max 50 | 배송사 고유 코드 (부분 수정 시 전달, 중복 불가) | +| name | body | array | 아니오 | — | 대상의 이름/명칭 | +| type | body | string | 아니오 | — | 배송사 유형 (`domestic` 국내 / `international` 해외) | +| tracking_url | body | string | 아니오 | max 500 | 배송 추적 URL 템플릿 (`{tracking_number}` 치환자를 운송장 번호로 대체) | +| is_active | body | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_carrier.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 기존 배송사 정보를 수정합니다. `sirsoft-ecommerce.settings.update` 권한이 필요하며, `ShippingCarrierService::updateCarrier()` 가 코드·다국어명·유형·추적 URL·활성여부·정렬순서 중 전달된 값을 갱신하고 수정된 리소스를 반환합니다(모든 body 필드는 선택적, 부분 수정 가능). 대상 배송사가 없거나 갱신에 실패하면 각각 404/400 을 반환합니다. 배송사의 추적 URL이나 표시명을 변경할 때 사용합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/shipping-carriers/{id}/toggle-status + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-carriers.toggle-status` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingCarrierController@toggleStatus` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 배송사 1건의 활성 상태를 토글합니다. `sirsoft-ecommerce.settings.update` 권한이 필요하며, `ShippingCarrierService::toggleStatus()` 가 현재 활성 여부를 반전시키고 갱신된 배송사 리소스를 반환합니다. 대상 배송사가 없거나 처리에 실패하면 각각 404/400 을 반환합니다. 목록 화면에서 배송사 사용여부 스위치를 켜고 끌 때 사용합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-country.md b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-country.md new file mode 100644 index 00000000..0d30179e --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-country.md @@ -0,0 +1,76 @@ +# Shipping Country API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Shipping Country 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/user/shipping-country + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.shipping-country.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserShippingCountryController@show` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| preferred_shipping_country | null | `null` | 회원이 저장한 선호 배송국가 코드 (2자리 대문자, 미설정 시 `null`) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 로그인한 회원이 마이페이지에 저장해 둔 선호 배송국가 코드를 조회합니다. `auth:sanctum` 인증이 필요하며, `UserShippingCountryService::getPreferredShippingCountry()`가 인증 사용자 ID로 영속된 값을 반환하고 미설정 시 `preferred_shipping_country: null`을 내려줍니다. 마이페이지 배송국가 설정·회원정보 수정 화면이 초기 선택값을 채우는 데 사용합니다. + + +### PUT /api/modules/sirsoft-ecommerce/user/shipping-country + +- **라우트명**: `api.modules.sirsoft-ecommerce.user.shipping-country.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserShippingCountryController@update` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| shipping_country | body | string | 예 | — | 저장할 선호 배송국가 2자리 코드 (활성 배송가능 국가만 허용, 대문자로 정규화되어 저장) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 로그인한 회원이 선호 배송국가를 저장합니다. `auth:sanctum` 인증이 필요하며, `UpdateUserShippingCountryRequest`가 활성 국가 코드만 허용하도록 검증한 뒤 `UserShippingCountryService::setPreferredShippingCountry()`가 인증 사용자에게 영속합니다. 저장된 값은 대문자로 정규화되어 응답되며, 이후 장바구니·주문 계산의 기본 배송국가로 사용됩니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-policies.md b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-policies.md new file mode 100644 index 00000000..33f1c20c --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-policies.md @@ -0,0 +1,389 @@ +# Shipping Policies API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Shipping Policies 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/admin/shipping-policies + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-policies.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingPolicyController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| search | query | string | 아니오 | max 200 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| shipping_methods | query | array | 아니오 | — | 배송방법 코드로 필터 (ShippingType 코드 배열, 국가별 설정 중 하나라도 매치되는 정책만) | +| charge_policies | query | array | 아니오 | — | 배송비 부과정책으로 필터 (free/fixed/conditional_free/range_*/api/per_* 등, 국가별 설정 매치) | +| countries | query | array | 아니오 | — | 배송 국가 코드로 필터 (ISO 코드 배열, 해당 국가 설정을 가진 정책만) | +| is_active | query | string | 아니오 | ``, `true`, `false` | 활성 여부 (true 활성 / false 비활성) | +| sort_by | query | string | 아니오 | `id`, `name`, `is_active`, `sort_order`, `created_at`, `updated_at` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | +| per_page | query | integer | 아니오 | min 10, max 100 | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_policy.list_validation_rules`, `sirsoft-ecommerce.shipping_policy.list_validation_messages`). + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| number | integer | `17` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) | +| id | integer | `47` | 기본 키 (내부 식별자) | +| 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-07 14:47:31` | 생성 일시 | +| updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 배송정책 목록을 페이지네이션으로 조회합니다. `sirsoft-ecommerce.shipping-policies.read` 권한이 필요하며, `ShippingPolicyService::getList()` 가 검색어·배송방식·부과정책·국가·활성여부 필터와 정렬을 적용하고, 함께 `getStatistics()` 로 집계 통계를 계산해 `ShippingPolicyCollection` 에 담아 반환합니다. 각 항목의 `abilities` 로 생성/수정/삭제 가능 여부가 내려옵니다. 배송정책 관리 목록 화면을 채우는 데 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/shipping-policies + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-policies.store` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingPolicyController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| is_active | body | boolean | 예 | — | 활성 여부 (true 활성 / false 비활성) | +| is_default | body | boolean | 아니오 | — | 기본값 지정 여부 | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | +| country_settings | body | array | 예 | min 1 | 국가별 배송 설정 배열 (최소 1개). 각 항목에 국가코드·배송방식·부과정책(charge_policy)·배송비·구간/API/도서산간 설정을 담음 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_policy.store_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 새 배송정책을 생성합니다. `sirsoft-ecommerce.shipping-policies.create` 권한이 필요하며, `ShippingPolicyService::create()` 가 다국어 정책명(`name`), 활성여부, 기본여부, 정렬순서, 국가별 설정(`country_settings`, 최소 1개)을 저장하고 201 로 생성된 정책 리소스를 반환합니다. 국가별로 배송방식·배송비 부과정책을 담은 배송정책을 새로 등록할 때 사용합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/shipping-policies/active + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-policies.active` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingPolicyController@activeList` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| value | integer | `46` | 배송정책 ID (Select 옵션의 value) | +| label | string | `sdfsf` | 표시용 라벨 | +| countries_display | string | `🇰🇷` | 활성 배송 국가를 국기 이모지로 표시한 문자열 (최대 3개, 초과분 `+N`) | +| fee_summary | string | `KR: 외부 API 연동 (실시간 계산)` | 국가별 배송비 요약 텍스트 (`country_code: fee` 형태를 ` \| ` 로 결합) | +| is_default | boolean | `false` | default 여부 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.read`)이 없는 경우 | + + + +**설명** 활성화된 배송정책만 Select 옵션 형태로 조회합니다. `sirsoft-ecommerce.shipping-policies.read` 권한이 필요하며, `ShippingPolicyService::getActiveList()` 결과를 `{value, label, countries_display, fee_summary, is_default}` 로 매핑해 반환합니다. 상품 등록/수정 폼 등에서 배송정책을 선택하는 드롭다운을 채우는 데 사용하며, 비활성 정책은 노출되지 않습니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/shipping-policies/bulk + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-policies.bulk-destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingPolicyController@bulkDestroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | query | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_policy.bulk_delete_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.delete`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 선택한 여러 배송정책을 한 번에 삭제합니다. `sirsoft-ecommerce.shipping-policies.delete` 권한이 필요하며, `ShippingPolicyService::bulkDelete()` 가 `ids`(최소 1개)에 해당하는 정책들을 삭제하고 `deleted_count` 를 반환합니다. 목록 화면에서 체크박스로 다건 선택 후 일괄 삭제할 때 사용합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/shipping-policies/bulk-toggle-active + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-policies.bulk-toggle-active` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingPolicyController@bulkToggleActive` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| is_active | body | boolean | 예 | — | 활성 여부 (true 활성 / false 비활성) | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_policy.bulk_toggle_active_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 선택한 여러 배송정책의 활성 상태를 한 번에 변경합니다. `sirsoft-ecommerce.shipping-policies.update` 권한이 필요하며, `ShippingPolicyService::bulkToggleActive()` 가 `ids`(최소 1개)에 해당하는 정책들을 `is_active` 값으로 일괄 활성/비활성 처리하고 `updated_count` 를 반환합니다. 목록 화면에서 다건 선택 후 사용여부를 한꺼번에 켜거나 끌 때 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/admin/shipping-policies/test-api-call + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-policies.test-api-call` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingPolicyController@testApiCall` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| endpoint | body | string | 예 | max 500 | 테스트로 호출할 외부 배송비 계산 API 엔드포인트 URL | +| request_fields | body | array | 아니오 | — | 요청에 실어 보낼 필드명 목록 (후보 SSoT ShippingApiRequestField 5종) | +| config | body | array | 아니오 | — | API 호출 고급 설정 (HTTP 메서드·인증방식·필드 매핑·응답 형식/경로 등) | +| sample | body | array | 아니오 | — | 테스트 계산에 사용할 샘플 주문 데이터 (무게/금액/수량 등) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자가 배송정책 편집 폼에서 입력 중인 설정으로 외부 배송비 계산 API 를 1회 실호출해 미리 테스트합니다. `sirsoft-ecommerce.shipping-policies.update` 권한이 필요하며, `OrderCalculationService::testApiCall()` 이 `endpoint`·`config`·`request_fields`·`sample` 을 사용해 실제 요청을 보내고 요청 미리보기, 응답, 추출된 배송비를 반환합니다. 타임아웃과 응답 크기 제한이 적용됩니다. 실시간 계산형 배송정책을 저장하기 전에 API 연동이 올바른지 검증할 때 사용합니다. + + +### DELETE /api/modules/sirsoft-ecommerce/admin/shipping-policies/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-policies.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingPolicyController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 배송정책 1건을 삭제합니다. `sirsoft-ecommerce.shipping-policies.delete` 권한이 필요하며, 컨트롤러가 `getDetail()` 로 정책을 조회해 없으면 404 를 반환한 뒤 `ShippingPolicyService::delete()` 로 제거합니다. 사용 중인 정책이라 삭제할 수 없는 등 실패 시 400 을 반환합니다. 더 이상 쓰지 않는 배송정책을 개별 정리할 때 사용합니다. + + +### GET /api/modules/sirsoft-ecommerce/admin/shipping-policies/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-policies.show` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingPolicyController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 배송정책 1건의 상세 정보를 조회합니다. `sirsoft-ecommerce.shipping-policies.read` 권한이 필요하며, `ShippingPolicyService::getDetail()` 이 정책을 조회해 없으면 404 를 반환하고, 있으면 다국어 정책명·국가별 설정·배송비 요약 등을 담은 단건 리소스를 반환합니다. 배송정책 수정 화면 진입 시 기존 값을 불러오는 데 사용합니다. + + +### PUT /api/modules/sirsoft-ecommerce/admin/shipping-policies/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-policies.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingPolicyController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | +| name | body | array | 예 | — | 대상의 이름/명칭 | +| is_active | body | boolean | 예 | — | 활성 여부 (true 활성 / false 비활성) | +| is_default | body | boolean | 아니오 | — | 기본값 지정 여부 | +| sort_order | body | integer | 아니오 | min 0 | 표시 정렬 순서 값 (작을수록 우선) | +| country_settings | body | array | 예 | min 1 | 국가별 배송 설정 배열 (최소 1개). 각 항목에 국가코드·배송방식·부과정책(charge_policy)·배송비·구간/API/도서산간 설정을 담음 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.shipping_policy.update_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 기존 배송정책을 수정합니다. `sirsoft-ecommerce.shipping-policies.update` 권한이 필요하며, 컨트롤러가 `getDetail()` 로 정책을 조회해 없으면 404 를 반환한 뒤 `ShippingPolicyService::update()` 가 다국어 정책명·활성여부·기본여부·정렬순서·국가별 설정(`country_settings`, 최소 1개)을 갱신하고 수정된 리소스를 반환합니다. 배송정책의 국가별 배송비/방식을 변경할 때 사용합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/shipping-policies/{id}/set-default + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-policies.set-default` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingPolicyController@setDefault` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 지정한 배송정책을 기본 배송정책으로 설정합니다. `sirsoft-ecommerce.shipping-policies.update` 권한이 필요하며, 컨트롤러가 `getDetail()` 로 정책을 조회해 없으면 404 를 반환한 뒤 `ShippingPolicyService::setDefault()` 가 해당 정책을 기본값으로 지정합니다(기존 기본 정책은 해제). 별도 정책이 매칭되지 않을 때 적용되는 기본 배송정책을 바꿀 때 사용합니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/shipping-policies/{id}/toggle-active + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.shipping-policies.toggle-active` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\ShippingPolicyController@toggleActive` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.shipping-policies.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 배송정책 1건의 활성 상태를 토글합니다. `sirsoft-ecommerce.shipping-policies.update` 권한이 필요하며, 컨트롤러가 `getDetail()` 로 정책을 조회해 없으면 404 를 반환한 뒤 `ShippingPolicyService::toggleActive()` 가 현재 활성 여부를 반전시키고 갱신된 리소스를 반환합니다. 목록 화면에서 개별 정책의 사용여부 스위치를 켜고 끌 때 사용합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/users.md b/modules/_bundled/sirsoft-ecommerce/docs/api/users.md new file mode 100644 index 00000000..45a6639a --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/users.md @@ -0,0 +1,81 @@ +# Users API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Users 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### PATCH /api/modules/sirsoft-ecommerce/admin/users/{user}/currency + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.users.currency.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\AdminUserCurrencyController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.user-currency.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| user | path | string | 예 | — | 대상 user의 식별자 | +| currency | body | string | 예 | — | 회원에게 지정할 결제 통화 코드 (등록 통화: is_default 또는 exchange_rate>0 인 통화만 허용) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-currency.manage`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 특정 회원의 선호 결제 통화를 변경합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.user-currency.manage` 권한이 필요하며, path의 `{user}`는 UUID 라우트 모델 바인딩(관리자 회원 URL 규약)으로 주입됩니다. `UserCurrencyService::changeUserCurrencyByAdmin()`이 통화 저장과 활동 로그 훅 발화를 한 단위로 처리하며, `currency`는 등록된 통화 코드만 허용됩니다. 관리자 회원 상세 화면에서 회원별 결제 통화를 지정하는 용도입니다. + + +### PATCH /api/modules/sirsoft-ecommerce/admin/users/{user}/shipping-country + +- **라우트명**: `api.modules.sirsoft-ecommerce.admin.users.shipping-country.update` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\AdminUserShippingCountryController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.user-shipping-country.manage` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| user | path | string | 예 | — | 대상 user의 식별자 | +| shipping_country | body | string | 예 | — | 회원에게 지정할 배송국가 2자리 코드 (활성 배송가능 국가만 허용, 대문자로 정규화) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-shipping-country.manage`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자가 특정 회원의 선호 배송국가를 변경합니다. `auth:sanctum` + admin + `sirsoft-ecommerce.user-shipping-country.manage` 권한이 필요하며, path의 `{user}`는 UUID 라우트 모델 바인딩으로 주입됩니다. `UserShippingCountryService::changeUserShippingCountryByAdmin()`이 배송국가 저장과 활동 로그 훅 발화를 한 단위로 처리하고, `shipping_country`는 활성 국가만 허용되며 대문자로 정규화되어 반환됩니다. 관리자 회원 상세 화면에서 회원별 기본 배송국가를 지정하는 용도입니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/wishlist.md b/modules/_bundled/sirsoft-ecommerce/docs/api/wishlist.md new file mode 100644 index 00000000..1b099ed6 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/wishlist.md @@ -0,0 +1,104 @@ +# Wishlist API 레퍼런스 + +> **소유**: module `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Wishlist 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-ecommerce/wishlist + +- **라우트명**: `api.modules.sirsoft-ecommerce.wishlist.index` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\WishlistController@index` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 로그인한 회원의 찜(위시리스트) 목록을 페이지네이션으로 조회합니다. `auth:sanctum` 인증이 필요하며, `WishlistController@index`가 `ProductWishlistService::getByUser()`로 본인 찜 목록만 가져와 `WishlistCollection`으로 반환합니다. `per_page`는 기본 20건이며 최대 100건으로 제한됩니다. 마이페이지의 찜 목록 화면에서 사용합니다. + + +### POST /api/modules/sirsoft-ecommerce/wishlist/toggle + +- **라우트명**: `api.modules.sirsoft-ecommerce.wishlist.toggle` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\WishlistController@toggle` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| product_id | body | integer | 예 | — | product 식별자 | + +> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.wishlist.toggle_validation_rules`). + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 특정 상품(`product_id`)의 찜 상태를 토글합니다. `auth:sanctum` 인증이 필요하며, `WishlistController@toggle`이 `ProductWishlistService::toggle()`을 호출해 이미 찜한 상품이면 제거하고 아니면 추가한 뒤 `added` 불리언을 반환합니다. 상품 상세/목록의 찜 하트 버튼이 이 하나의 엔드포인트로 추가·제거를 모두 처리합니다. 응답의 `added` 값으로 현재 찜 여부를 즉시 갱신할 수 있습니다. + + +### DELETE /api/modules/sirsoft-ecommerce/wishlist/{id} + +- **라우트명**: `api.modules.sirsoft-ecommerce.wishlist.destroy` +- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Public\WishlistController@destroy` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 찜 목록에서 특정 찜 항목(`id`)을 삭제합니다. `auth:sanctum` 인증이 필요하며, `WishlistController@destroy`가 `ProductWishlistService::destroy()`로 본인 소유 찜만 삭제합니다. 상품 ID가 아니라 찜 레코드 ID로 삭제하며, 해당 항목이 본인 것이 아니거나 존재하지 않으면 404를 반환합니다. 상품 상세의 하트 토글과 달리 마이페이지 찜 목록에서 특정 항목을 명시적으로 제거할 때 사용합니다. + + diff --git a/modules/_bundled/sirsoft-ecommerce/src/Support/ApiDoc/ApiDocSampleService.php b/modules/_bundled/sirsoft-ecommerce/src/Support/ApiDoc/ApiDocSampleService.php new file mode 100644 index 00000000..4efed001 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Support/ApiDoc/ApiDocSampleService.php @@ -0,0 +1,511 @@ +}> 도메인 => 대표 레코드 정보 + */ + public function seed(): array + { + $actor = $this->sampleActor(); + + $product = $this->seedProduct(); + $category = $this->seedCategory(); + $order = $actor ? $this->seedOrder($actor) : null; + $review = ($actor && $product) ? $this->seedReview($actor, $product) : null; + $inquiry = ($actor && $product) ? $this->seedInquiry($actor, $product) : null; + $address = $actor ? $this->seedAddress($actor) : null; + $mileage = $actor ? $this->seedMileageTransaction($actor) : null; + + $brand = $this->seedBrand(); + $coupon = $this->seedCoupon(); + $shippingPolicy = $this->seedShippingPolicy(); + $carrier = $this->seedShippingCarrier(); + $claimReason = $this->seedClaimReason(); + $label = $this->seedProductLabel(); + $commonInfo = $this->seedProductCommonInfo(); + $noticeTemplate = $this->seedNoticeTemplate(); + $extraFee = $this->seedExtraFeeTemplate(); + + $map = []; + + // products: 공개 상세(products/{id}) 는 숫자 id + VISIBLE, admin 상세는 id/product_code 허용. + if ($product) { + $pid = (string) $product->getKey(); + $map['products'] = [ + 'model' => Product::class, + 'key' => 'id', + 'value' => $pid, + 'path_params' => [ + 'id' => $pid, + 'identifier' => $pid, + 'code' => (string) $product->product_code, + 'productId' => $pid, + 'product' => $pid, + ], + ]; + } + + // categories: 공개는 slug, admin 은 id. + if ($category) { + $cid = (string) $category->getKey(); + $map['categories'] = [ + 'model' => Category::class, + 'key' => 'id', + 'value' => $cid, + 'path_params' => [ + 'id' => $cid, + 'slug' => (string) $category->slug, + 'category' => $cid, + ], + ]; + } + + // orders: 회원 주문 상세(user/orders/{id}) + 주문번호 조회(user/orders/{orderNumber}). + if ($order) { + $oid = (string) $order->getKey(); + $map['orders'] = [ + 'model' => Order::class, + 'key' => 'id', + 'value' => $oid, + 'path_params' => [ + 'id' => $oid, + 'order' => $oid, + 'orderNumber' => (string) $order->order_number, + ], + ]; + } + + // reviews: admin 상세는 route-model binding(id), 공개 상품 리뷰 목록은 productId. + if ($review) { + $rid = (string) $review->getKey(); + $map['reviews'] = [ + 'model' => ProductReview::class, + 'key' => 'id', + 'value' => $rid, + 'path_params' => [ + 'review' => $rid, + 'id' => $rid, + 'orderOptionId' => (string) ($review->order_option_id ?? ''), + ], + ]; + } + + // inquiries: 상품 문의 목록 실측용 대표 문의(products/{productId}/inquiries 는 products 맵 사용). + if ($inquiry) { + $iid = (string) $inquiry->getKey(); + $map['inquiries'] = [ + 'model' => ProductInquiry::class, + 'key' => 'id', + 'value' => $iid, + 'path_params' => ['id' => $iid, 'inquiry' => $iid], + ]; + } + + // addresses: 본인 배송지 상세(user/addresses/{id}). + if ($address) { + $aid = (string) $address->getKey(); + $map['addresses'] = [ + 'model' => UserAddress::class, + 'key' => 'id', + 'value' => $aid, + 'path_params' => ['id' => $aid], + ]; + } + + // mileage-transactions: 연관 거래 조회(admin/mileage-transactions/{id}/linked). + if ($mileage) { + $mid = (string) $mileage->getKey(); + $map['mileage-transactions'] = [ + 'model' => MileageTransaction::class, + 'key' => 'id', + 'value' => $mid, + 'path_params' => ['id' => $mid], + ]; + } + + // 단순 id 상세 도메인(admin/{domain}/{id}). + $this->putSimpleIdDomain($map, 'brands', Brand::class, $brand); + $this->putSimpleIdDomain($map, 'promotion-coupons', Coupon::class, $coupon); + $this->putSimpleIdDomain($map, 'shipping-policies', ShippingPolicy::class, $shippingPolicy); + $this->putSimpleIdDomain($map, 'shipping-carriers', ShippingCarrier::class, $carrier); + $this->putSimpleIdDomain($map, 'claim-reasons', ClaimReason::class, $claimReason); + $this->putSimpleIdDomain($map, 'product-labels', ProductLabel::class, $label); + $this->putSimpleIdDomain($map, 'product-common-infos', ProductCommonInfo::class, $commonInfo); + $this->putSimpleIdDomain($map, 'product-notice-templates', ProductNoticeTemplate::class, $noticeTemplate); + $this->putSimpleIdDomain($map, 'extra-fee-templates', ExtraFeeTemplate::class, $extraFee); + + // settings: 레코드 없이 유효 카테고리 문자열 고정(admin/settings/{category}). + $map['settings'] = [ + 'model' => Product::class, + 'key' => 'id', + 'value' => $product ? (string) $product->getKey() : '1', + 'path_params' => ['category' => 'basic_info'], + ]; + + return $map; + } + + /** + * 단순 id 상세 도메인 항목을 맵에 등록합니다(모델이 null 이면 건너뜀). + * + * @param array $map 대상 맵(참조) + * @param string $domain 도메인 그룹명 + * @param class-string $model 모델 FQCN + * @param Model|null $record 대표 레코드 + */ + private function putSimpleIdDomain(array &$map, string $domain, string $model, $record): void + { + if (! $record) { + return; + } + + $id = (string) $record->getKey(); + $map[$domain] = [ + 'model' => $model, + 'key' => 'id', + 'value' => $id, + 'path_params' => ['id' => $id], + ]; + } + + /** + * 완전 샘플 상품을 멱등 생성합니다(전시 상태로 공개 상세 실측 가능). + * + * @return Product|null 대표 상품 (생성 실패 시 null) + */ + private function seedProduct(): ?Product + { + $product = Product::query()->where('product_code', self::SAMPLE_PRODUCT_CODE)->first(); + if ($product) { + return $product; + } + + return Product::factory()->create([ + 'name' => ['ko' => 'API 문서 샘플 상품', 'en' => 'API Doc Sample Product'], + 'product_code' => self::SAMPLE_PRODUCT_CODE, + 'sales_status' => 'on_sale', + 'display_status' => 'visible', + ]); + } + + /** + * 완전 샘플 카테고리를 멱등 생성합니다(공개 slug 조회 실측 가능). + * + * @return Category|null 대표 카테고리 (생성 실패 시 null) + */ + private function seedCategory(): ?Category + { + $category = Category::query()->where('slug', self::SAMPLE_CATEGORY_SLUG)->first(); + if ($category) { + return $category; + } + + return Category::query()->create([ + 'name' => ['ko' => 'API 문서 샘플 카테고리', 'en' => 'API Doc Sample Category'], + 'slug' => self::SAMPLE_CATEGORY_SLUG, + 'path' => '0', + 'depth' => 0, + 'sort_order' => 0, + 'is_active' => true, + ]); + } + + /** + * 실측 사용자 소유의 완전 샘플 주문을 멱등 생성합니다. + * + * @param User $actor 실측 사용자 + * @return Order|null 대표 주문 (생성 실패 시 null) + */ + private function seedOrder(User $actor): ?Order + { + $order = Order::query()->where('user_id', $actor->id) + ->where('order_number', 'like', 'APIDOC-%') + ->first(); + if ($order) { + return $order; + } + + return Order::factory()->forUser($actor)->create([ + 'order_number' => 'APIDOC-'.now()->format('Ymd').'-000001', + ]); + } + + /** + * 완전 샘플 상품 리뷰를 멱등 생성합니다. + * + * @param User $actor 실측 사용자 + * @param Product $product 대표 상품 + * @return ProductReview|null 대표 리뷰 (생성 실패 시 null) + */ + private function seedReview(User $actor, Product $product): ?ProductReview + { + $review = ProductReview::query()->where('product_id', $product->id) + ->where('user_id', $actor->id)->first(); + if ($review) { + return $review; + } + + return ProductReview::factory()->create([ + 'product_id' => $product->id, + 'user_id' => $actor->id, + ]); + } + + /** + * 완전 샘플 상품 문의를 멱등 생성합니다. + * + * @param User $actor 실측 사용자 + * @param Product $product 대표 상품 + * @return ProductInquiry|null 대표 문의 (생성 실패 시 null) + */ + private function seedInquiry(User $actor, Product $product): ?ProductInquiry + { + $inquiry = ProductInquiry::query()->where('product_id', $product->id) + ->where('user_id', $actor->id)->first(); + if ($inquiry) { + return $inquiry; + } + + return ProductInquiry::factory()->create([ + 'product_id' => $product->id, + 'user_id' => $actor->id, + ]); + } + + /** + * 실측 사용자 소유의 완전 샘플 배송지를 멱등 생성합니다. + * + * @param User $actor 실측 사용자 + * @return UserAddress|null 대표 배송지 (생성 실패 시 null) + */ + private function seedAddress(User $actor): ?UserAddress + { + $address = UserAddress::query()->where('user_id', $actor->id) + ->where('name', 'API 문서 샘플 배송지')->first(); + if ($address) { + return $address; + } + + return UserAddress::factory()->create([ + 'user_id' => $actor->id, + 'name' => 'API 문서 샘플 배송지', + ]); + } + + /** + * 실측 사용자 소유의 완전 샘플 마일리지 거래를 멱등 생성합니다. + * + * @param User $actor 실측 사용자 + * @return MileageTransaction|null 대표 거래 (생성 실패 시 null) + */ + private function seedMileageTransaction(User $actor): ?MileageTransaction + { + $tx = MileageTransaction::query()->where('user_id', $actor->id) + ->where('type', 'admin_earn')->first(); + if ($tx) { + return $tx; + } + + return MileageTransaction::query()->create([ + 'user_id' => $actor->id, + 'type' => 'admin_earn', + 'amount' => 1000, + 'balance_after' => 1000, + ]); + } + + /** + * 완전 샘플 브랜드를 멱등 생성합니다. + * + * @return Brand|null 대표 브랜드 (생성 실패 시 null) + */ + private function seedBrand(): ?Brand + { + return Brand::query()->firstOrCreate( + ['slug' => 'apidoc-sample-brand'], + ['name' => ['ko' => 'API 문서 샘플 브랜드', 'en' => 'API Doc Sample Brand']] + ); + } + + /** + * 완전 샘플 프로모션 쿠폰을 멱등 생성합니다. + * + * @return Coupon|null 대표 쿠폰 (생성 실패 시 null) + */ + private function seedCoupon(): ?Coupon + { + return Coupon::query()->firstOrCreate( + ['name->ko' => 'API 문서 샘플 쿠폰'], + [ + 'name' => ['ko' => 'API 문서 샘플 쿠폰', 'en' => 'API Doc Sample Coupon'], + 'target_type' => 'order_amount', + 'discount_type' => 'fixed', + 'discount_value' => 1000, + 'issue_method' => 'download', + 'issue_condition' => 'manual', + ] + ); + } + + /** + * 완전 샘플 배송정책을 멱등 생성합니다. + * + * @return ShippingPolicy|null 대표 배송정책 (생성 실패 시 null) + */ + private function seedShippingPolicy(): ?ShippingPolicy + { + return ShippingPolicy::query()->firstOrCreate( + ['name->ko' => 'API 문서 샘플 배송정책'], + ['name' => ['ko' => 'API 문서 샘플 배송정책', 'en' => 'API Doc Sample Shipping Policy']] + ); + } + + /** + * 완전 샘플 배송사를 멱등 생성합니다. + * + * @return ShippingCarrier|null 대표 배송사 (생성 실패 시 null) + */ + private function seedShippingCarrier(): ?ShippingCarrier + { + return ShippingCarrier::query()->firstOrCreate( + ['code' => 'apidoc'], + [ + 'name' => ['ko' => 'API 문서 샘플 배송사', 'en' => 'API Doc Sample Carrier'], + 'type' => 'domestic', + ] + ); + } + + /** + * 완전 샘플 클레임 사유를 멱등 생성합니다. + * + * @return ClaimReason|null 대표 클레임 사유 (생성 실패 시 null) + */ + private function seedClaimReason(): ?ClaimReason + { + return ClaimReason::query()->firstOrCreate( + ['type' => 'refund', 'code' => 'apidoc_sample'], + [ + 'name' => ['ko' => 'API 문서 샘플 사유', 'en' => 'API Doc Sample Reason'], + 'fault_type' => 'customer', + ] + ); + } + + /** + * 완전 샘플 상품 라벨을 멱등 생성합니다. + * + * @return ProductLabel|null 대표 라벨 (생성 실패 시 null) + */ + private function seedProductLabel(): ?ProductLabel + { + return ProductLabel::query()->firstOrCreate( + ['name->ko' => 'API 문서 샘플 라벨'], + ['name' => ['ko' => 'API 문서 샘플 라벨', 'en' => 'API Doc Sample Label']] + ); + } + + /** + * 완전 샘플 상품 공통정보를 멱등 생성합니다. + * + * @return ProductCommonInfo|null 대표 공통정보 (생성 실패 시 null) + */ + private function seedProductCommonInfo(): ?ProductCommonInfo + { + return ProductCommonInfo::query()->firstOrCreate( + ['name->ko' => 'API 문서 샘플 공통정보'], + ['name' => ['ko' => 'API 문서 샘플 공통정보', 'en' => 'API Doc Sample Common Info']] + ); + } + + /** + * 완전 샘플 상품정보고시 템플릿을 멱등 생성합니다. + * + * @return ProductNoticeTemplate|null 대표 고시 템플릿 (생성 실패 시 null) + */ + private function seedNoticeTemplate(): ?ProductNoticeTemplate + { + return ProductNoticeTemplate::query()->firstOrCreate( + ['name->ko' => 'API 문서 샘플 고시템플릿'], + [ + 'name' => ['ko' => 'API 문서 샘플 고시템플릿', 'en' => 'API Doc Sample Notice Template'], + 'fields' => [['label' => '품명', 'value' => '샘플']], + ] + ); + } + + /** + * 완전 샘플 추가배송비 템플릿을 멱등 생성합니다. + * + * @return ExtraFeeTemplate|null 대표 추가배송비 템플릿 (생성 실패 시 null) + */ + private function seedExtraFeeTemplate(): ?ExtraFeeTemplate + { + return ExtraFeeTemplate::query()->firstOrCreate( + ['zipcode' => '00000'], + ['fee' => 3000] + ); + } + + /** + * 샘플 작성자로 쓸 사용자를 반환합니다. + * + * 코어 완전 샘플 사용자(먼저 시드됨)를 우선하고, 없으면 첫 사용자로 폴백합니다. + * + * @return User|null 샘플 사용자 (없으면 null) + */ + private function sampleActor(): ?User + { + return User::query()->where('email', 'apidoc-sample-user@example.com')->first() + ?? User::query()->orderBy('id')->first(); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Support/ApiDocSampleServiceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Support/ApiDocSampleServiceTest.php new file mode 100644 index 00000000..9c12ada8 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Support/ApiDocSampleServiceTest.php @@ -0,0 +1,120 @@ +assertInstanceOf(ApiDocSampleSeeder::class, new ApiDocSampleService); + } + + #[Test] + public function 이커머스_핵심_도메인_대표_샘플_맵을_반환한다(): void + { + // 회원 소유 도메인(orders/reviews/inquiries/addresses/mileage)은 실측 사용자를 + // 전제한다 — 실측 커맨드 컨텍스트(코어 완전 샘플 사용자)를 재현하기 위해 먼저 생성. + User::factory()->create(); + + $map = (new ApiDocSampleService)->seed(); + + // 핵심 상세 GET 대상 도메인 키가 모두 존재 + foreach (['products', 'categories', 'orders', 'reviews', 'settings'] as $domain) { + $this->assertArrayHasKey($domain, $map, "도메인 '{$domain}' 누락"); + } + + $this->assertSame(Product::class, $map['products']['model']); + $this->assertNotEmpty($map['products']['value']); + } + + #[Test] + public function 문자열_path_param_실측용_맵을_제공한다(): void + { + $map = (new ApiDocSampleService)->seed(); + + // products: 공개 show(id)·admin show(identifier/code)·상품 하위(productId) + $product = Product::query()->where('product_code', self::SAMPLE_PRODUCT_CODE)->firstOrFail(); + $params = $map['products']['path_params']; + + $this->assertSame((string) $product->getKey(), $params['id']); + $this->assertSame((string) $product->getKey(), $params['identifier']); + $this->assertSame(self::SAMPLE_PRODUCT_CODE, $params['code']); + $this->assertSame((string) $product->getKey(), $params['productId']); + + // categories: 공개 show 는 slug + $this->assertSame(self::SAMPLE_CATEGORY_SLUG, $map['categories']['path_params']['slug']); + + // settings: 레코드 없이 유효 카테고리 문자열 고정 + $this->assertSame('basic_info', $map['settings']['path_params']['category']); + } + + #[Test] + public function 공개_상세_실측용_상품은_전시_상태다(): void + { + (new ApiDocSampleService)->seed(); + + $product = Product::query()->where('product_code', self::SAMPLE_PRODUCT_CODE)->firstOrFail(); + + // products/{id} 공개 show 는 display_status===visible 필터 → 실측 가능해야 함 + $this->assertSame('visible', $product->display_status->value); + $this->assertSame('on_sale', $product->sales_status->value); + } + + #[Test] + public function 공개_slug_조회용_카테고리는_활성이며_slug를_갖는다(): void + { + (new ApiDocSampleService)->seed(); + + $category = Category::query()->where('slug', self::SAMPLE_CATEGORY_SLUG)->firstOrFail(); + + $this->assertTrue((bool) $category->is_active); + $this->assertSame(self::SAMPLE_CATEGORY_SLUG, $category->slug); + } + + #[Test] + public function 재실행_시_샘플이_중복_생성되지_않는다(): void + { + $service = new ApiDocSampleService; + + $service->seed(); + $service->seed(); + + $this->assertSame( + 1, + Product::query()->where('product_code', self::SAMPLE_PRODUCT_CODE)->count() + ); + $this->assertSame( + 1, + Category::query()->where('slug', self::SAMPLE_CATEGORY_SLUG)->count() + ); + } +} diff --git a/modules/_bundled/sirsoft-page/docs/api/attachments.md b/modules/_bundled/sirsoft-page/docs/api/attachments.md new file mode 100644 index 00000000..c7565987 --- /dev/null +++ b/modules/_bundled/sirsoft-page/docs/api/attachments.md @@ -0,0 +1,109 @@ +# Attachments API 레퍼런스 + +> **소유**: module `sirsoft-page` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Attachments 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### POST /api/modules/sirsoft-page/admin/attachments + +- **라우트명**: `api.modules.sirsoft-page.admin.attachments.upload` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageAttachmentController@upload` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| file | body | file | 예 | max 10240 | 업로드 파일 | +| page_id | body | integer | 아니오 | min 1 | page 식별자 | +| collection | body | string | 아니오 | max 100 | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) | +| temp_key | body | string | 아니오 | max 64 | 저장 전 임시 귀속 키. 아직 저장되지 않은 페이지의 첨부를 임시로 묶어 두고, 이후 페이지 저장 시 이 키로 확정 귀속합니다 (`page_id` 미지정 시 사용) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 페이지 첨부파일을 업로드합니다(최대 10MB). `file`(필수)과 함께 이미 저장된 페이지에 귀속시키려면 `page_id`, 아직 저장 전이면 `temp_key`(임시 귀속 후 페이지 저장 시 `store`/`update` 의 `temp_key` 로 확정)를 보냅니다. `collection` 으로 첨부 그룹을 구분합니다. 응답은 FileUploader 컴포넌트 규약(`data.data`)에 맞춰 `PageAttachmentResource` 를 201 로 반환합니다. 권한은 `pages.create` 를 요구합니다. + + +### PATCH /api/modules/sirsoft-page/admin/attachments/reorder + +- **라우트명**: `api.modules.sirsoft-page.admin.attachments.reorder` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageAttachmentController@reorder` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| order | body | array | 예 | min 1 | 새 정렬 순서 배열. 각 원소는 `{"id": 첨부ID(정수), "order": 순서값(0 이상 정수)}` 형태이며, 이 순서대로 첨부 표시 순위가 갱신됩니다 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 첨부파일 정렬 순서를 변경합니다. `order` 는 FileUploader 가 보내는 `[{id, order}]` 형태의 배열이며, 컨트롤러가 `[ID => order]` 매핑으로 변환해 `PageAttachmentService::reorder()` 에 전달합니다. 편집 화면에서 첨부 목록을 드래그로 재정렬할 때 사용합니다. 권한은 `pages.update` 를 요구합니다. + + +### DELETE /api/modules/sirsoft-page/admin/attachments/{id} + +- **라우트명**: `api.modules.sirsoft-page.admin.attachments.destroy` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageAttachmentController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | string | 예 | — | 대상 리소스의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 첨부파일을 삭제합니다. `{id}` 는 첨부파일 ID 입니다(라우트-모델 바인딩이 아닌 `int $id`). `PageAttachmentService::deleteAttachment()` 가 DB 레코드와 실제 저장 파일을 함께 정리합니다. 미존재 시 404 를 반환합니다. 권한은 `pages.update` 를 요구합니다. + + diff --git a/modules/_bundled/sirsoft-page/docs/api/pages.md b/modules/_bundled/sirsoft-page/docs/api/pages.md new file mode 100644 index 00000000..7e6925ce --- /dev/null +++ b/modules/_bundled/sirsoft-page/docs/api/pages.md @@ -0,0 +1,490 @@ +# Pages API 레퍼런스 + +> **소유**: module `sirsoft-page` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Pages 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/modules/sirsoft-page/admin/pages + +- **라우트명**: `api.modules.sirsoft-page.admin.pages.index` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| published | query | boolean | 아니오 | — | 발행 여부 (발행된 항목만 필터) | +| search | query | string | 아니오 | max 100 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | +| search_field | query | string | 아니오 | `all`, `title`, `slug` | 검색 대상 필드명 (검색어를 적용할 컬럼) | +| filters | query | array | 아니오 | — | 추가 필터 조건 맵 (필드별 조건) | +| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| sort_by | query | string | 아니오 | `created_at`, `published_at` | 정렬 기준 필드명 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (asc 오름차순 / desc 내림차순) | + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `29` | 기본 키 (내부 식별자) | +| slug | string | `apidoc-sample-page` | URL 슬러그 (고유) | +| title | string | `API 문서 샘플 페이지` | 페이지 제목 (다국어 JSON) | +| published | boolean | `true` | 발행 여부 (true: 발행, false: 미발행) | +| published_at | string | `2026-07-06 23:57:18` | published 일시 | +| current_version | integer | `2` | 현재 버전 번호 | +| creator | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| created_at | string | `2026-07-06 23:57:18` | 생성 일시 | +| updated_at | string | `2026-07-06 23:57:18` | 최종 수정 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 관리자 페이지 목록을 조회합니다. `published`, `search`/`search_field`(제목·슬러그), `filters` 로 필터링하고 `sort_by`(created_at, published_at)·`sort_order` 로 정렬하며 `per_page`·`page` 로 페이지네이션합니다. `PageService::getPages()` 가 반환하는 `PageCollection` 으로 래핑되어 목록 항목마다 `is_owner`·`abilities` 권한 메타가 함께 내려갑니다. 관리자 페이지 관리 화면(목록 그리드)의 데이터 소스입니다. + + +### POST /api/modules/sirsoft-page/admin/pages + +- **라우트명**: `api.modules.sirsoft-page.admin.pages.store` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | body | string | 예 | max 255 | URL 친화 식별자 (slug) | +| title | body | array | 예 | — | 제목 | +| content | body | string | 아니오 | — | 본문 내용 | +| content_mode | body | string | 아니오 | `html`, `text` | 본문 편집 모드. `html` 은 리치 에디터 HTML, `text` 는 평문으로 저장·렌더링됩니다 (미지정 시 `html`) | +| published | body | boolean | 아니오 | — | 발행 여부 (발행된 항목만 필터) | +| seo_meta | body | array | 아니오 | — | SEO 메타 정보 맵. 하위 키 `title`(max 255)·`description`(max 500)·`keywords`(max 500)를 담습니다 | +| temp_key | body | string | 아니오 | max 64 | 저장 전 첨부 업로드 시 발급받은 임시 키. 생성된 페이지에 임시 첨부를 귀속시키는 데 사용합니다 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 새 페이지를 생성합니다. `slug`(고유 필수)·`title`(다국어 배열 필수)·`content`·`content_mode`(html/text)·`published`·`seo_meta` 를 받아 `PageService::createPage()` 로 저장하고, `creator`/`updater`/`attachments` 를 eager load 한 `PageResource` 를 201 로 반환합니다. `temp_key` 는 페이지 저장 전 임시 업로드한 첨부(첨부 업로드 시 발급받은 키)를 생성된 페이지에 귀속시키는 데 씁니다. + + +### PATCH /api/modules/sirsoft-page/admin/pages/bulk-publish + +- **라우트명**: `api.modules.sirsoft-page.admin.pages.bulk-publish` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageController@bulkPublish` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) | +| published | body | boolean | 예 | — | 발행 여부 (발행된 항목만 필터) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 여러 페이지의 발행 상태를 한 번에 변경합니다. `ids`(대상 페이지 ID 배열, 최소 1개)와 `published`(true=발행, false=미발행)를 받아 `PageService::bulkChangePublishStatus()` 로 일괄 적용하고, 실제 변경된 건수를 `count` 로 반환합니다. 목록 화면의 다중 선택 후 일괄 발행/미발행 액션에 사용합니다. + + +### POST /api/modules/sirsoft-page/admin/pages/check-slug + +- **라우트명**: `api.modules.sirsoft-page.admin.pages.check-slug` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageController@checkSlug` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.create` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | body | string | 예 | max 255 | URL 친화 식별자 (slug) | +| exclude_id | body | integer | 아니오 | min 1 | exclude 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.create`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 슬러그 중복 여부를 확인합니다. `slug`(필수)와 선택 `exclude_id`(수정 화면에서 자기 자신을 중복 검사에서 제외)를 받아 `{ "exists": true|false }` 를 반환합니다. 페이지 생성/수정 폼에서 슬러그 입력 시 실시간 중복 검증에 사용합니다. 권한은 `pages.create` 를 요구합니다. + + +### DELETE /api/modules/sirsoft-page/admin/pages/{page} + +- **라우트명**: `api.modules.sirsoft-page.admin.pages.destroy` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageController@destroy` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | path | string | 예 | — | 조회할 페이지 번호 (1부터 시작) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.delete`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 페이지를 소프트 삭제합니다. `{page}` 는 페이지 ID 입니다(라우트-모델 바인딩이 아닌 `int $id` 로 서비스에서 조회). 존재하지 않으면 404, 스코프 권한 밖이면 403 을 반환합니다. 삭제는 DB CASCADE 가 아니라 `PageService::deletePage()` 를 통해 수행되어 관련 훅·정리 로직이 보장됩니다. 권한은 `pages.delete` 를 요구합니다. + + +### GET /api/modules/sirsoft-page/admin/pages/{page} + +- **라우트명**: `api.modules.sirsoft-page.admin.pages.show` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | path | string | 예 | — | 조회할 페이지 번호 (1부터 시작) | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `29` | 기본 키 (내부 식별자) | +| slug | string | `apidoc-sample-page` | URL 슬러그 (고유) | +| title | object | `{"ko":"API 문서 샘플 페이지","en":"API Doc Sample Page"}` | 페이지 제목 (다국어 JSON) | +| content | object | `{"ko":"

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

Co…` | 페이지 본문 (다국어 JSON) | +| content_mode | string | `html` | 본문 형식 (html, text) | +| published | boolean | `true` | 발행 여부 (true: 발행, false: 미발행) | +| published_at | string | `2026-07-06 23:57:18` | published 일시 | +| seo_meta | object | `{"title":"Et qui sapiente veritatis.","description":"Et m…` | SEO 메타 정보 (title, description, keywords) | +| current_version | integer | `2` | 현재 버전 번호 | +| creator | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| updater | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 최종 수정자 정보 객체 (uuid/name — updater 관계 파생, 로드된 경우에만 포함) | +| attachments | array | `[{"id":9,"hash":"xtfalwomchas","original_filename":"deser…` | 페이지에 귀속된 첨부파일 목록 (PageAttachmentResource 배열 — id/hash/원본파일명/URL 등, 로드된 경우에만 포함) | +| created_at | string | `2026-07-06 23:57:18` | 생성 일시 | +| updated_at | string | `2026-07-06 23:57:18` | 최종 수정 일시 | +| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 관리자용 페이지 상세를 조회합니다. `{page}` 는 페이지 ID 입니다. `creator`·`updater`·`attachments` 관계를 eager load 한 `PageResource` 를 반환하며, 목록과 달리 `content`(다국어 본문)·`seo_meta` 전체를 포함합니다. 페이지 편집 화면 진입 시 폼 초기값 로딩에 사용합니다. 미존재 404, 스코프 밖 403. + + +### PUT /api/modules/sirsoft-page/admin/pages/{page} + +- **라우트명**: `api.modules.sirsoft-page.admin.pages.update` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | path | string | 예 | — | 조회할 페이지 번호 (1부터 시작) | +| slug | body | string | 아니오 | max 255 | URL 친화 식별자 (slug) | +| title | body | array | 예 | — | 제목 | +| content | body | string | 아니오 | — | 본문 내용 | +| content_mode | body | string | 아니오 | `html`, `text` | 본문 편집 모드. `html` 은 리치 에디터 HTML, `text` 는 평문으로 저장·렌더링됩니다 (미지정 시 `html`) | +| published | body | boolean | 아니오 | — | 발행 여부 (발행된 항목만 필터) | +| seo_meta | body | array | 아니오 | — | SEO 메타 정보 맵. 하위 키 `title`(max 255)·`description`(max 500)·`keywords`(max 500)를 담습니다 | +| temp_key | body | string | 아니오 | max 64 | 저장 전 첨부 업로드 시 발급받은 임시 키. 새로 업로드한 첨부를 이 페이지에 귀속시키는 데 사용합니다 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 페이지를 수정합니다. `{page}` 는 페이지 ID 입니다. `title`(필수)·`slug`·`content`·`content_mode`·`published`·`seo_meta` 를 받아 `PageService::updatePage()` 로 반영하고, 수정 시 이전 상태가 버전 이력으로 적재됩니다(`current_version` 증가). `creator`/`updater`/`attachments` 를 eager load 한 `PageResource` 를 반환합니다. 미존재 404, 스코프 밖 403. + + +### PATCH /api/modules/sirsoft-page/admin/pages/{page}/publish + +- **라우트명**: `api.modules.sirsoft-page.admin.pages.publish` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageController@publish` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | path | string | 예 | — | 조회할 페이지 번호 (1부터 시작) | +| published | body | boolean | 예 | — | 발행 여부 (발행된 항목만 필터) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 단일 페이지의 발행 상태만 토글합니다. `{page}` 는 페이지 ID, `published`(필수 boolean)로 발행/미발행을 전환합니다. 본문·슬러그 등 다른 필드는 건드리지 않으며 변경된 `PageResource` 를 반환합니다. 목록/상세 화면의 발행 토글 스위치에 사용합니다. 미존재 404, 스코프 밖 403. + + +### GET /api/modules/sirsoft-page/admin/pages/{page}/versions + +- **라우트명**: `api.modules.sirsoft-page.admin.pages.versions.index` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageController@versions` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | path | string | 예 | — | 조회할 페이지 번호 (1부터 시작) | + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `40` | 기본 키 (내부 식별자) | +| page_id | integer | `29` | page 식별자 (연관 리소스 참조) | +| version | integer | `2` | 버전 순번 (1부터 증가, 페이지 수정 시마다 채번) | +| title | object | `{"ko":"API 문서 샘플 페이지 (v2)","en":"API Doc Sample Page (v2)"}` | 페이지 제목 (다국어 JSON) | +| content | object | `{"ko":"

수정 버전 본문.<\/p>","en":"

Revised version body.<…` | 페이지 본문 (다국어 JSON) | +| content_mode | string | `html` | 본문 형식 (html, text) | +| seo_meta | null | `null` | SEO 메타 정보 (title, description, keywords) | +| changes_summary | string | `본문 보강` | 이 버전에서 무엇이 바뀌었는지 요약한 변경 설명 (버전 생성 시 기록, 없으면 null) | +| creator | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) | +| created_at | string | `2026-07-06 23:57:18` | 생성 일시 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 페이지의 버전 이력 목록을 조회합니다. `{page}` 는 페이지 ID 입니다. 각 항목은 그 시점의 `title`·`content`·`content_mode`·`seo_meta` 스냅샷과 `version` 번호·`changes_summary`(변경 요약)·`creator`(작성자)를 담습니다. 편집 화면의 버전 이력 패널에서 과거 버전 목록을 보여 주는 데이터 소스입니다. 미존재 404, 스코프 밖 403. + + +### GET /api/modules/sirsoft-page/admin/pages/{page}/versions/{versionId} + +- **라우트명**: `api.modules.sirsoft-page.admin.pages.versions.show` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageController@showVersion` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | path | string | 예 | — | 조회할 페이지 번호 (1부터 시작) | +| versionId | path | string | 예 | — | 대상 version의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 버전의 상세 스냅샷을 조회합니다. `{page}` 는 페이지 ID, `{versionId}` 는 버전 ID 입니다. 해당 버전의 전체 본문(`content`)과 메타를 담은 `PageVersionResource` 를 반환하며, 복원 전 미리보기·버전 간 비교에 사용합니다. `{versionId}` 는 라우트-모델 바인딩이 아니어서 문서 생성 시 자동 실측에서 제외되며, 응답 shape 은 `versions.index` 항목과 동일합니다. 미존재 404, 스코프 밖 403. + + +### POST /api/modules/sirsoft-page/admin/pages/{page}/versions/{versionId}/restore + +- **라우트명**: `api.modules.sirsoft-page.admin.pages.versions.restore` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\Admin\PageController@restoreVersion` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-page.pages.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | path | string | 예 | — | 조회할 페이지 번호 (1부터 시작) | +| versionId | path | string | 예 | — | 대상 version의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 페이지를 특정 버전 시점으로 복원합니다. `{page}` 는 페이지 ID, `{versionId}` 는 복원 대상 버전 ID 입니다. `PageService::restoreVersion()` 이 그 버전의 내용을 현재 페이지에 반영하고(복원 자체도 새 버전으로 적재), 복원된 `PageResource` 를 반환합니다. 편집 화면 버전 이력 패널의 "이 버전으로 복원" 액션에 사용합니다. 미존재 404, 스코프 밖 403. + + +### GET /api/modules/sirsoft-page/pages/attachment/{hash} + +- **라우트명**: `api.modules.sirsoft-page.pages.attachment.download` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\User\PublicPageAttachmentController@download` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 공개 첨부파일을 다운로드합니다(해시 기반, 12자). 실제 미들웨어는 `optional.sanctum` 으로, 비로그인 사용자도 접근할 수 있습니다(위 표의 `auth:sanctum` 은 생성기가 sanctum 미들웨어를 인식한 표기이며, 실제로는 토큰이 없어도 통과). 발행된 페이지의 첨부는 누구나, 미발행 페이지의 첨부는 `sirsoft-page.pages.read` 관리자만 다운로드할 수 있고, 그 외에는 404 로 존재를 숨깁니다. 브라우저 직접 GET(토큰 미탑재)을 위해 해시 라우트로 단일화되었습니다. 파일 스트리밍 응답이므로 실측 대상이 아닙니다. + + +### GET /api/modules/sirsoft-page/pages/attachment/{hash}/preview + +- **라우트명**: `api.modules.sirsoft-page.pages.attachment.preview` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\User\PublicPageAttachmentController@preview` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| hash | path | string | 예 | — | 대상 리소스의 해시 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 이미지 첨부 썸네일을 인라인으로 미리봅니다(해시 기반, 12자). 썸네일 `` 는 토큰을 실을 수 없으므로 발행/미발행과 무관하게 공개 서빙합니다. 미발행 콘텐츠의 썸네일은 해시를 보유해야만 조회 가능(비추측성)하며, 실제 파일 다운로드는 `download` 의 권한 게이트로 보호됩니다. 이미지 스트리밍 응답이므로 실측 대상이 아닙니다. + + +### GET /api/modules/sirsoft-page/pages/{slug} + +- **라우트명**: `api.modules.sirsoft-page.pages.show` +- **컨트롤러**: `Modules\Sirsoft\Page\Http\Controllers\User\PublicPageController@show` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 발행된 페이지를 슬러그로 조회하는 공개 엔드포인트입니다. 실제 미들웨어는 `optional.sanctum` 으로 비로그인 사용자도 발행 페이지를 볼 수 있습니다(위 표의 `auth:sanctum` 은 생성기 표기이며 토큰 없이도 통과). `sirsoft-page.pages.read` 권한을 가진 관리자는 미발행 페이지도 사용자 화면에서 미리볼 수 있으며(이때 응답에 preview 표시가 실림), 비로그인·일반 회원은 미발행 시 404 를 받습니다. `PublicPageResource` 는 관리자용 상세보다 축소된 공개 표면을 노출하고 `attachments` 를 포함합니다. `{slug}` 는 문자열 라우트 파라미터라 문서 생성 시 자동 실측에서 제외됩니다. + + diff --git a/modules/_bundled/sirsoft-page/src/Support/ApiDoc/ApiDocSampleService.php b/modules/_bundled/sirsoft-page/src/Support/ApiDoc/ApiDocSampleService.php new file mode 100644 index 00000000..62eaa3bd --- /dev/null +++ b/modules/_bundled/sirsoft-page/src/Support/ApiDoc/ApiDocSampleService.php @@ -0,0 +1,113 @@ + 도메인 => 대표 레코드 정보 + */ + public function seed(): array + { + $map = []; + + $page = $this->seedPage(); + $map['pages'] = ['model' => Page::class, 'key' => $page->getRouteKeyName(), 'value' => (string) $page->getRouteKey()]; + + return $map; + } + + /** + * 완전한 발행 페이지 샘플(버전 이력 + 첨부 포함)을 생성합니다. + * + * @return Page 대표 페이지 레코드 + */ + private function seedPage(): Page + { + $page = Page::query()->where('slug', self::SAMPLE_SLUG)->first(); + + if ($page) { + return $page; + } + + $actor = $this->sampleActor(); + + $page = Page::factory()->published()->withSeoMeta()->create([ + 'slug' => self::SAMPLE_SLUG, + 'title' => ['ko' => 'API 문서 샘플 페이지', 'en' => 'API Doc Sample Page'], + 'content' => [ + 'ko' => '

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

', + 'en' => '

Complete sample page body for API reference probing.

', + ], + 'content_mode' => 'html', + 'current_version' => 2, + 'created_by' => $actor?->id, + 'updated_by' => $actor?->id, + ]); + + // 버전 이력 2건 — versions 조회 실측이 배열 항목 필드를 확보하도록. + PageVersion::factory()->create([ + 'page_id' => $page->id, + 'version' => 1, + 'title' => ['ko' => 'API 문서 샘플 페이지 (v1)', 'en' => 'API Doc Sample Page (v1)'], + 'content' => ['ko' => '

초기 버전 본문.

', 'en' => '

Initial version body.

'], + 'content_mode' => 'html', + 'changes_summary' => '최초 작성', + 'created_by' => $actor?->id, + ]); + + PageVersion::factory()->create([ + 'page_id' => $page->id, + 'version' => 2, + 'title' => ['ko' => 'API 문서 샘플 페이지 (v2)', 'en' => 'API Doc Sample Page (v2)'], + 'content' => ['ko' => '

수정 버전 본문.

', 'en' => '

Revised version body.

'], + 'content_mode' => 'html', + 'changes_summary' => '본문 보강', + 'created_by' => $actor?->id, + ]); + + // 첨부 1건 — 첨부 임베드 응답 필드가 채워지도록. + PageAttachment::factory()->image()->create([ + 'page_id' => $page->id, + 'created_by' => $actor?->id, + ]); + + return $page->refresh(); + } + + /** + * 샘플 소유자/작성자로 쓸 사용자를 반환합니다. + * + * 코어 완전 샘플 사용자(먼저 시드됨)를 우선하고, 없으면 첫 사용자로 폴백합니다. + * + * @return User|null 샘플 사용자 (없으면 null) + */ + private function sampleActor(): ?User + { + return User::query()->where('email', 'apidoc-sample-user@example.com')->first() + ?? User::query()->orderBy('id')->first(); + } +} diff --git a/modules/_bundled/sirsoft-page/tests/Unit/Support/ApiDocSampleServiceTest.php b/modules/_bundled/sirsoft-page/tests/Unit/Support/ApiDocSampleServiceTest.php new file mode 100644 index 00000000..081a0bfd --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/Unit/Support/ApiDocSampleServiceTest.php @@ -0,0 +1,65 @@ +assertInstanceOf(ApiDocSampleSeeder::class, new ApiDocSampleService); + } + + #[Test] + public function 페이지_도메인_대표_샘플_맵을_반환한다(): void + { + $map = (new ApiDocSampleService)->seed(); + + $this->assertArrayHasKey('pages', $map); + $this->assertSame(Page::class, $map['pages']['model']); + $this->assertArrayHasKey('key', $map['pages']); + $this->assertNotEmpty($map['pages']['value']); + } + + #[Test] + public function 대표_샘플_페이지는_발행_상태로_버전_이력과_첨부를_갖는다(): void + { + (new ApiDocSampleService)->seed(); + + $page = Page::query()->where('slug', 'apidoc-sample-page')->first(); + + $this->assertNotNull($page); + $this->assertTrue($page->published); + $this->assertNotNull($page->published_at); + $this->assertNotNull($page->seo_meta); + $this->assertSame(2, $page->versions()->count()); + $this->assertSame(1, $page->attachments()->count()); + } + + #[Test] + public function 재실행_시_샘플이_중복_생성되지_않는다(): void + { + $service = new ApiDocSampleService; + + $service->seed(); + $countAfterFirst = Page::query()->where('slug', 'apidoc-sample-page')->count(); + + $service->seed(); + $countAfterSecond = Page::query()->where('slug', 'apidoc-sample-page')->count(); + + $this->assertSame(1, $countAfterFirst); + $this->assertSame(1, $countAfterSecond); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/docs/api/images.md b/plugins/_bundled/sirsoft-ckeditor5/docs/api/images.md new file mode 100644 index 00000000..5b40deb9 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/docs/api/images.md @@ -0,0 +1,58 @@ +# Images API 레퍼런스 + +> **소유**: plugin `sirsoft-ckeditor5` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Images 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/plugins/sirsoft-ckeditor5/images/{hash} + +- **라우트명**: `api.plugins.sirsoft-ckeditor5.api.sirsoft-ckeditor5.images.serve` +- **컨트롤러**: `Plugins\Sirsoft\Ckeditor5\Http\Controllers\ImageServeController@serve` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| hash | path | string | 예 | — | 업로드 시 발급된 12자리 소문자 16진수 이미지 해시(라우트 제약 `[a-f0-9]{12}`). 저장된 `` 의 마지막 경로 세그먼트로, 이 값으로 서빙할 이미지를 조회한다. | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** + +CKEditor5 본문에 삽입된 이미지를 실제로 내려주는 공개 서빙 엔드포인트다. 에디터가 저장한 HTML 의 `` 를 브라우저가 직접 GET 하므로 **인증이 없다**(`PublicBaseController`). 발행된 콘텐츠를 비로그인 독자가 열람하는 시나리오를 지원하기 위한 설계다. + +- `{hash}` 는 라우트 제약 `where('hash', '[a-f0-9]{12}')` 로 12자리 소문자 16진수만 매칭된다. 형식이 어긋나면 라우트 자체가 매칭되지 않아 404 가 된다. 이 값은 이미지 업로드 시 발급된 `download_url` 의 마지막 경로 세그먼트다. +- 성공 응답은 JSON 이 아니라 **이미지 바이너리 스트림**(`StreamedResponse`, `Content-Type` 은 이미지 MIME)이다. `ResponseHelper` envelope 로 감싸지 않는다. +- `ImageServeService::findByHash()` 가 레코드를 찾지 못하거나 스토리지에 실제 파일이 없어 `serve()` 가 null 을 반환하면 `messages.image.not_found`(404) 표준 JSON 을 반환한다. + +**응답 예시** (실패 시에만 JSON — 성공 시에는 이미지 바이너리) + +```json +{ "success": false, "data": null, "message": "이미지를 찾을 수 없습니다.", "error": { "code": "not_found" } } +``` + + diff --git a/plugins/_bundled/sirsoft-ckeditor5/docs/api/upload.md b/plugins/_bundled/sirsoft-ckeditor5/docs/api/upload.md new file mode 100644 index 00000000..a8dfb862 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/docs/api/upload.md @@ -0,0 +1,66 @@ +# Upload API 레퍼런스 + +> **소유**: plugin `sirsoft-ckeditor5` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Upload 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### POST /api/plugins/sirsoft-ckeditor5/upload + +- **라우트명**: `api.plugins.sirsoft-ckeditor5.api.sirsoft-ckeditor5.upload` +- **컨트롤러**: `Plugins\Sirsoft\Ckeditor5\Http\Controllers\ImageUploadController@upload` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| upload | body | file | 예 | max 2048 | 에디터에 드롭/붙여넣은 이미지 파일 1개(multipart). 허용 MIME 은 `jpeg,jpg,png,gif,webp`, 최대 크기는 플러그인 설정 `imageMaxSizeMb`(기본 2MB) 로 결정된다. | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +CKEditor5 의 SimpleUploadAdapter 가 에디터에 드롭/붙여넣은 이미지를 업로드하는 관리자 엔드포인트다. 컨트롤러가 `AdminBaseController` 를 상속하므로 실제 인증은 `auth:sanctum` **에 더해 관리자(admin) 미들웨어**가 적용된다(생성기 표기는 `auth:sanctum` 만 노출). + +- **응답 형식이 표준 envelope 가 아니다.** SimpleUploadAdapter 규격상 성공 시 HTTP 201 + 최상위 `{"url": "..."}`, 실패 시 4xx/5xx + `{"error": {"message": "..."}}` 를 반환한다. `ResponseHelper` 를 쓰지 않으므로 `data`/`success` 필드가 없다. +- **요청 파라미터**: multipart body 의 `upload` 필드(이미지 파일 1개). 허용 MIME 은 `jpeg,jpg,png,gif,webp`, 최대 크기는 플러그인 설정 `imageMaxSizeMb`(기본 2MB) 로 동적 결정된다. 검증 실패도 CKEditor 규격(`{"error":{"message":...}}`, HTTP 422)으로 응답한다. +- **선택 권한 게이트**: query 파라미터 `permission` 이 주어지면, 현재 사용자가 해당 권한을 갖지 못한 경우 403 `{"error":{"message":...}}`. 에디터를 임베드하는 화면이 업로드 권한을 세분화할 때 사용한다. +- 업로드 성공 시 반환하는 `url` 은 공개 서빙 엔드포인트(`GET /images/{hash}`)의 절대 URL 이다. + +**응답 예시** (성공 — CKEditor 규격, envelope 아님) + +```json +{ "url": "https://example.com/api/plugins/sirsoft-ckeditor5/images/a1b2c3d4e5f6" } +``` + +**오류 예시** (검증/권한/서버 오류 공통 — HTTP 422/403/500) + +```json +{ "error": { "message": "이미지 파일만 업로드할 수 있습니다." } } +``` + + diff --git a/plugins/_bundled/sirsoft-gdpr/docs/api/consent-log.md b/plugins/_bundled/sirsoft-gdpr/docs/api/consent-log.md new file mode 100644 index 00000000..43b40ee1 --- /dev/null +++ b/plugins/_bundled/sirsoft-gdpr/docs/api/consent-log.md @@ -0,0 +1,61 @@ +# Consent Log API 레퍼런스 + +> **소유**: plugin `sirsoft-gdpr` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Consent Log 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/plugins/sirsoft-gdpr/admin/consent-log + +- **라우트명**: `api.plugins.sirsoft-gdpr.admin.consent-log.index` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\Admin\GdprAdminConsentLogController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-gdpr.privacy.view` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `83` | 기본 키 (내부 식별자) | +| user_id | integer | `130` | user 식별자 (연관 리소스 참조) | +| session_id | string | `c50916f6-58ee-42a6-a046-60a0c9ecfad2` | session 식별자 (연관 리소스 참조) | +| consent_key | string | `cookie_marketing` | 변경된 동의 항목 키 (쿠키 카테고리 키). 어떤 동의 항목의 부여/철회가 기록된 행인지 나타냅니다 | +| action | string | `revoked` | 이 이력 행의 변경 유형 (`granted`=동의 부여, `revoked`=철회, `acknowledged`=확인). Art.7(1) 입증 트레일의 행위 구분입니다 | +| source | string | `banner` | 동의 변경이 발생한 경로 (`banner`=쿠키 배너, `preference_center`=환경설정 센터, `mypage`=마이페이지, 그 외 `register`/`order`/`withdraw`) | +| policy_version | string | `10` | 변경 시점의 정책 버전 문자열. 해당 동의가 어느 정책 버전 기준으로 표명되었는지 기록해 정책 갱신 후 재동의 판정과 감사에 사용됩니다 | +| categories | object | `{"cookie_analytics":false,"cookie_marketing":false,"cooki…` | 변경 시점 전체 카테고리별 동의 여부 스냅샷 (키→boolean). MySQL JSON 컬럼이라 키가 알파벳 순으로 정규화 저장됩니다 | +| categories_snapshot | array | `[{"key":"cookie_necessary","label_key":"sirsoft-gdpr.cons…` | `categories` 객체를 관리자 화면 iteration 친화 배열(`{key, label_key, granted}`)로 변환한 것. 필수→분석→마케팅 UX 위계 순으로 재정렬되며 `label_key`는 프론트가 다국어로 해석합니다 | +| ip_address | string | `127.0.0.1` | 요청/행위가 발생한 IP 주소 | +| user_agent | string | `Mozilla/5.0 (Windows NT 10.0; Win64; …` | 동의 변경 요청의 User-Agent 문자열. DPO 감사 시 동의 표명 단말/브라우저를 식별하는 용도이며 회원 삭제 시 NULL로 익명화됩니다 | +| created_at | string | `2026-07-03 00:24:27` | 생성 일시 | +| user | object | `{"id":130,"uuid":"a21d03b4-df0b-4aa6-ab7a-f51143f6375a","…` | 동의 주체 회원 정보(id/uuid/name/email). 관계가 로드된 경우에만 포함되며 게스트 이력이거나 삭제로 익명화된 경우 null입니다 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-gdpr.privacy.view`)이 없는 경우 | + + + +**설명** 관리자 감사 화면에서 `gdpr_user_consent_histories` 테이블의 동의 로그를 페이지네이션으로 조회합니다. `auth:sanctum`과 `sirsoft-gdpr.privacy.view` 권한이 필요합니다. `email`, `session_id`, `consent_keys[]`, `actions[]`(granted|revoked), `sources[]`(banner|preference_center|mypage), `per_page`(1~100, 기본 20) 쿼리 필터를 지원합니다. DPO 감사 용도로 IP 주소와 User-Agent까지 노출되는 조회 전용 엔드포인트입니다. + + diff --git a/plugins/_bundled/sirsoft-gdpr/docs/api/consent.md b/plugins/_bundled/sirsoft-gdpr/docs/api/consent.md new file mode 100644 index 00000000..80cba515 --- /dev/null +++ b/plugins/_bundled/sirsoft-gdpr/docs/api/consent.md @@ -0,0 +1,209 @@ +# Consent API 레퍼런스 + +> **소유**: plugin `sirsoft-gdpr` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Consent 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### POST /api/plugins/sirsoft-gdpr/consent/cookie + +- **라우트명**: `api.plugins.sirsoft-gdpr.consent.cookie` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\Public\GdprCookieConsentController@store` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 공개 쿠키 동의 배너에서 방문자가 선택한 카테고리별 동의를 저장합니다. `optional.sanctum` 라우트로 게스트와 회원 모두 호출할 수 있으며, 회원(sanctum 토큰 보유)이면 user_id 기준으로 status를 upsert하고 history를 남기고, 게스트면 session_id 기준으로 history를 기록합니다. 게스트가 처음 호출해 세션 식별자가 없으면 UUID 기반 `gdpr_session` 쿠키(1년, SameSite=Lax)를 응답에 발급해 첨부합니다. 동의 철회 시 실제 쿠키 파기는 이 엔드포인트가 아니라 클라이언트 정리기와 후속 응답의 CookieConsentMiddleware가 담당합니다. + + +### GET /api/plugins/sirsoft-gdpr/consent/cookie/status + +- **라우트명**: `api.plugins.sirsoft-gdpr.consent.cookie.status` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\Public\GdprCookieConsentController@status` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| has_consented | boolean | `false` | consented 여부 | +| consents | array | `[]` | 현재 방문자의 활성 쿠키 동의 목록(카테고리별 현재 선택 상태). 배너가 기존 동의를 반영해 토글 초기값을 그릴 때 사용합니다 | +| needs_renewal | boolean | `false` | 과거 동의 이력은 있으나 현재 정책 버전으로는 미동의인 상태(정책 갱신 후 재확인 필요). 동의 이력이 전혀 없는 신규 게스트는 `false`입니다 | +| current_policy_version | string | `10` | 현재 발행된 최신 정책 버전 문자열(정책 버전 서비스 기준). 방문자 동의 버전과 비교해 배너 재노출 여부를 판단합니다 | +| is_member | boolean | `true` | member 여부 | + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 현재 방문자의 쿠키 동의 상태를 반환해 배너 재노출 여부를 판단하게 합니다. `optional.sanctum` 라우트로 회원이면 user_id 기준, 미인증이면 session_id 기준으로 조회합니다. `has_consented`는 현재 정책 버전으로 동의를 완료했는지, `needs_renewal`은 과거 동의는 있으나 현재 정책 버전으로는 미동의인 상태(정책 갱신 후 재확인 필요)를 나타내며 동의 이력이 전혀 없는 신규 게스트는 `false`입니다. `is_member`는 배너가 회원 전용 분기(기존 동의 유지 버튼 등)를 노출할지 결정하는 데 사용합니다. + + +### POST /api/plugins/sirsoft-gdpr/consent/grant + +- **라우트명**: `api.plugins.sirsoft-gdpr.consent.grant` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\User\GdprConsentController@grant` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 마이페이지 「내 동의 현황」에서 회원이 특정 동의 항목을 부여하거나 다시 동의할 때 호출합니다. `auth:sanctum` 인증이 필요하며, `consent_key` 하나를 대상으로 동의를 `true`로 갱신하고 source를 `mypage`로 기록합니다. GDPR Art.7(3)의 자유 변경권을 구현한 것으로, 철회했던 항목의 재동의와 신규 동의를 동일하게 처리하며 history 행이 남습니다. `consent_key`의 화이트리스트 검사는 FormRequest에서 수행합니다. + + +### GET /api/plugins/sirsoft-gdpr/consent/history + +- **라우트명**: `api.plugins.sirsoft-gdpr.consent.history` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\User\GdprConsentController@history` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| histories | array | `[]` | 회원 본인의 동의 변경 이력 배열. 각 항목은 `consent_key`, `action`(granted/revoked), `source`, `policy_version`, `categories`, `created_at`로 구성되며 부여/철회 기록을 시간순으로 제공합니다 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 회원 본인의 동의 이력을 반환해 마이페이지 감사 목적으로 표시합니다. `auth:sanctum` 인증이 필요하며, `histories` 배열로 동의 부여/철회 기록을 시간순으로 제공합니다. 조회 전용이므로 부수 효과는 없습니다. + + +### GET /api/plugins/sirsoft-gdpr/consent/me + +- **라우트명**: `api.plugins.sirsoft-gdpr.consent.me` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\User\GdprConsentController@me` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| user_id | integer | `166` | user 식별자 (연관 리소스 참조) | +| needs_renewal | boolean | `false` | 회원의 활성 동의 중 옛 정책 버전인 항목이 있어 재동의가 필요한지 여부. 마이페이지가 「전체 다시 동의」 안내를 노출할지 판단합니다 | +| 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`)를 담아 한 화면에서 철회·재동의·신규 동의를 처리하게 합니다 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 마이페이지 「내 동의 현황」 화면의 데이터 소스로, 회원 본인의 전체 동의 매트릭스를 반환합니다. `auth:sanctum` 인증이 필요하며, 활성 동의만이 아니라 카탈로그의 모든 카테고리와 회원 상태를 합쳐 노출해 철회·재동의·신규 동의를 한 화면에서 처리하도록 합니다(Art.7(3) 대칭성). 함께 반환되는 `needs_renewal`과 `current_policy_version`으로 정책 버전 갱신 이후 재동의가 필요한지 판단합니다. 조회 전용입니다. + + +### POST /api/plugins/sirsoft-gdpr/consent/renew-all + +- **라우트명**: `api.plugins.sirsoft-gdpr.consent.renew_all` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\User\GdprConsentController@renewAll` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 정책 버전이 갱신된 뒤 회원의 활성 선택형 동의를 현재 정책 버전으로 일괄 재동의 처리합니다. `auth:sanctum` 인증이 필요하며, 필수 쿠키와 이미 철회한 항목은 대상에서 제외됩니다. 의사 변경 없이 재동의 의사 표명으로 처리되어 갱신된 각 항목마다 `action=granted` history 행이 누적됩니다(Art.7(1) 입증 트레일). 응답에는 갱신 건수가 포함되어 프론트 toast 메시지에 사용됩니다. + + +### POST /api/plugins/sirsoft-gdpr/consent/revoke + +- **라우트명**: `api.plugins.sirsoft-gdpr.consent.revoke` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\User\GdprConsentController@revoke` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** 마이페이지 「내 동의 현황」에서 회원이 특정 동의 항목을 철회할 때 호출합니다. `auth:sanctum` 인증이 필요하며, `consent_key` 하나를 대상으로 동의를 `false`로 갱신하고 source를 `mypage`로 기록하며 history 행을 남깁니다. `consent_key`의 화이트리스트 검사는 FormRequest에서 수행합니다. + + diff --git a/plugins/_bundled/sirsoft-gdpr/docs/api/policy-versions.md b/plugins/_bundled/sirsoft-gdpr/docs/api/policy-versions.md new file mode 100644 index 00000000..886b561e --- /dev/null +++ b/plugins/_bundled/sirsoft-gdpr/docs/api/policy-versions.md @@ -0,0 +1,144 @@ +# Policy Versions API 레퍼런스 + +> **소유**: plugin `sirsoft-gdpr` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Policy Versions 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/plugins/sirsoft-gdpr/admin/policy-versions + +- **라우트명**: `api.plugins.sirsoft-gdpr.admin.policy-versions.index` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\Admin\GdprAdminPolicyVersionController@index` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-gdpr.privacy.view` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `10` | 기본 키 (내부 식별자) | +| version | integer | `10` | 정책 버전 번호(단조 증가 정수). 발행 시마다 1씩 증가하며 회원 동의 시점의 버전과 비교해 재동의 필요 여부를 판정하는 기준입니다 | +| change_type | string | `material` | 변경 종류. `material`=카테고리 key/설명·slug 변경 등 모든 회원 재동의를 트리거하는 중대 변경, `non_material`=도메인/라벨/힌트 정정 등 재동의 미유발, `initial`=최초 발행(시드) | +| memo | string | `Chrome MCP 정밀 점검 — 마이페이지 정책 갱신 트리거 (P…` | 발행 사유 메모(최대 500자). 수동 발행 시 운영자가 입력하는 감사 추적용 설명으로 자동 감지 밖의 변경 배경을 남깁니다 | +| 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입니다 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-gdpr.privacy.view`)이 없는 경우 | + + + +**설명** 관리자 화면에서 발행된 개인정보 처리방침 정책 버전 이력을 version 내림차순 페이지네이션으로 조회합니다. `auth:sanctum`과 `sirsoft-gdpr.privacy.view` 권한이 필요합니다. `per_page`(1~100, 기본 20) 쿼리 파라미터로 페이지 크기를 조절할 수 있습니다. 조회 전용입니다. + + +### POST /api/plugins/sirsoft-gdpr/admin/policy-versions + +- **라우트명**: `api.plugins.sirsoft-gdpr.admin.policy-versions.store` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\Admin\GdprAdminPolicyVersionController@store` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-gdpr.privacy.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| memo | body | string | 예 | min 1, max 500 | 발행 사유 메모(1~500자, 필수). 자동 감지 밖의 변경을 인지하고 명시적으로 새 버전을 발행할 때 감사 추적용으로 남기는 설명입니다 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-gdpr.privacy.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 운영자가 새 개인정보 처리방침 정책 버전을 수동으로 발행합니다. `auth:sanctum`과 `sirsoft-gdpr.privacy.update` 권한이 필요하며, `memo`(1~500자)가 필수입니다. 자동 감지되는 변경(카테고리 key/설명·slug 변경) 밖의 변경(정책 본문 외부 수정, 법인명 변경 후 의도적 재동의 트리거 등)을 발행 시점의 현재 settings 스냅샷과 함께 새 버전으로 기록하며, settings 자체는 변경하지 않습니다. 이 발행이 회원의 `needs_renewal`을 true로 만드는 트리거가 됩니다. + + +### GET /api/plugins/sirsoft-gdpr/admin/policy-versions/current + +- **라우트명**: `api.plugins.sirsoft-gdpr.admin.policy-versions.current` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\Admin\GdprAdminPolicyVersionController@current` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-gdpr.privacy.view` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_목록 응답: `data.data[]` 배열 항목의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| uuid | string | `a1e0a91a-fba6-491c-a53e-7285a5686857` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) | +| name | string | `관리자` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) | +| email | string | `heuristing@gmail.com` | 이 버전을 발행한 운영자의 이메일. publisher 관계에서 노출되며 감사 화면에서 발행자를 식별하는 용도입니다 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-gdpr.privacy.view`)이 없는 경우 | + + + +**설명** 현재 발행된 최신 정책 버전을 반환합니다. `auth:sanctum`과 `sirsoft-gdpr.privacy.view` 권한이 필요합니다. 발행된 정책 버전 row가 하나도 없으면 `data`가 null입니다. 조회 전용입니다. + + +### GET /api/plugins/sirsoft-gdpr/admin/policy-versions/{version} + +- **라우트명**: `api.plugins.sirsoft-gdpr.admin.policy-versions.show` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\Admin\GdprAdminPolicyVersionController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-gdpr.privacy.view` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| version | path | string | 예 | — | 대상 버전 (버전 문자열) | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-gdpr.privacy.view`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** 특정 정책 버전의 상세(발행 시점 settings 스냅샷 본문 포함)를 반환합니다. `auth:sanctum`과 `sirsoft-gdpr.privacy.view` 권한이 필요합니다. `{version}`은 정수 path 파라미터로, 관리자 동의 이력·정책 버전 이력 화면에서 행 클릭 시 그 시점의 cookie_categories·privacy_policy_slug·blocked_domains를 모달로 표시해 회원 분쟁 시 당시 정책 본문을 확인할 수 있게 합니다(Art.7(1) 입증 책임). 해당 버전이 없으면 404를 반환합니다. + + diff --git a/plugins/_bundled/sirsoft-gdpr/docs/api/settings.md b/plugins/_bundled/sirsoft-gdpr/docs/api/settings.md new file mode 100644 index 00000000..7d2fab42 --- /dev/null +++ b/plugins/_bundled/sirsoft-gdpr/docs/api/settings.md @@ -0,0 +1,120 @@ +# Settings API 레퍼런스 + +> **소유**: plugin `sirsoft-gdpr` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Settings 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/plugins/sirsoft-gdpr/admin/settings + +- **라우트명**: `api.plugins.sirsoft-gdpr.admin.settings.show` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\Admin\GdprAdminSettingsController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-gdpr.privacy.view` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| settings | object | `{"privacy_policy_slug":"privacy","legal_entity_name":"","…` | GDPR 플러그인의 관리자 설정 전체 객체. 정책 메타데이터(slug/법인명/저장 위치), 배너 설정, 쿠키 카테고리 카탈로그, 차단 도메인을 담으며 `cookie_categories` 등 JSON 필드는 디코드되어 객체/배열로 노출됩니다 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-gdpr.privacy.view`)이 없는 경우 | + + + +**설명** GDPR 플러그인의 관리자 설정 전체를 반환해 관리자 설정 화면 폼에 바인딩합니다. `auth:sanctum`과 `sirsoft-gdpr.privacy.view` 권한이 필요합니다. `cookie_categories` 같은 JSON 필드는 디코드하여 객체/배열로 노출합니다. 조회 전용이며 정책 버전 발행 등 부수 효과는 없습니다. + + +### PUT /api/plugins/sirsoft-gdpr/admin/settings + +- **라우트명**: `api.plugins.sirsoft-gdpr.admin.settings.update` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\Admin\GdprAdminSettingsController@update` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-gdpr.privacy.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| privacy_policy_slug | body | string | 아니오 | max 100 | 개인정보 처리방침 페이지 slug(소문자·숫자·하이픈만). 비면 처리방침 링크가 비활성화되며 배너/마이페이지의 방침 링크 대상이 됩니다 | +| legal_entity_name | body | string | 아니오 | max 200 | legal entity 이름 (식별자) | +| data_storage_location | body | string | 아니오 | max 200 | 데이터 저장 위치 표기(GDPR Art.13(1)(f)/PIPA 국가 단위 안내용). IP/CIDR·클라우드 리전 코드 등 보안 민감 식별자는 검증에서 차단됩니다 | +| banner_enabled | body | boolean | 아니오 | — | 쿠키 배너와 자동 차단 엔진의 통합 활성 토글. `true`일 때만 배너 노출과 도메인 기반 차단이 동작합니다 | +| banner_position | body | string | 아니오 | `bottom_bar`, `bottom_left_popup`, `bottom_right_popup`, `centered_modal` | 쿠키 배너 노출 위치(하단 바/좌하단 팝업/우하단 팝업/중앙 모달) | +| cookie_categories | body | string | 아니오 | — | 쿠키 카테고리 카탈로그(JSON 문자열 또는 배열). 각 항목의 `key`는 necessary/functional/analytics/marketing 중 하나이며 `label.ko`/`label.en`이 필수입니다 | +| blocked_domains | body | array | 아니오 | — | 카테고리별 차단 도메인 패턴 배열(necessary 제외). FQDN 및 `*.` 와일드카드 prefix만 허용되며 textarea 줄바꿈 입력도 배열로 정규화됩니다 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-gdpr.privacy.update`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** 검증된 GDPR 관리자 설정을 저장합니다. `auth:sanctum`과 `sirsoft-gdpr.privacy.update` 권한이 필요합니다. 설정만 저장할 뿐 정책 버전은 자동 발행되지 않으며, 재동의가 필요한 변경이라면 운영자가 「+ 새 버전 발행」을 별도로 눌러야 합니다. 배너 문구·위치, 쿠키 카테고리, 차단 도메인 등을 갱신하는 쓰기 엔드포인트입니다. + + +### GET /api/plugins/sirsoft-gdpr/settings + +- **라우트명**: `api.plugins.sirsoft-gdpr.settings` +- **컨트롤러**: `Plugins\Sirsoft\Gdpr\Http\Controllers\Public\GdprSettingsController@show` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| cookie_policy_version | string | `10` | 현재 발행된 최신 정책 버전 문자열(`gdpr_policy_versions` 테이블이 SSoT, 최신 row의 version 정수). 게스트/회원 동의 시점 비교 및 배너 재노출 판정에 사용됩니다 | +| privacy_policy_slug | string | `privacy` | 개인정보 처리방침 페이지 slug. 설정이 비어 있으면 null로 반환됩니다 | +| privacy_policy_available | boolean | `true` | 처리방침 slug가 설정되어 링크 노출이 가능한지 여부. slug가 비면 `false`입니다 | +| legal_entity_name | string | `` | 개인정보 처리 법인/사업자명. 미설정 시 빈 문자열입니다 | +| data_storage_location | string | `` | 데이터 저장 위치 표기 문자열(국가 단위 안내). 미설정 시 빈 문자열입니다 | +| banner_enabled | boolean | `true` | 쿠키 배너와 자동 차단의 통합 활성 여부. `true`일 때만 게스트에게 배너가 노출되고 도메인 차단 엔진이 동작합니다 | +| banner_position | string | `bottom_bar` | 쿠키 배너 노출 위치(bottom_bar / bottom_left_popup / bottom_right_popup / centered_modal) | +| cookie_categories | array | `[{"key":"necessary","required":true,"label":{"ko":"필수 쿠키"…` | 배너·마이페이지 렌더링용 쿠키 카테고리 카탈로그 배열. 각 항목은 `key`, `required`(필수 여부), 다국어 `label`/`description`을 담습니다 | +| blocked_domains | object | `{"functional":["*.crisp.chat","widget.intercom.io"],"anal…` | 현재 설정된 카테고리별 차단 도메인 패턴(키→도메인 배열). 게스트도 차단이 동작해야 하므로 공개 응답에 노출되며 본 컨트롤러가 노출 SSoT입니다 | +| default_blocked_domains_preview | object | `{"functional":["*.crisp.chat","client.crisp.chat","*.inte…` | 플러그인 기본 차단 도메인 카탈로그 미리보기(변경 불가 기본값). 관리자가 커스텀 차단 목록을 편집할 때 참고용 기본 패턴을 보여줍니다 | + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** 게스트도 접근할 수 있는 완전 공개 엔드포인트로, 인증 없이 호출됩니다. 공개 쿠키 동의 배너와 마이페이지 카드 렌더링에 필요한 쿠키 카테고리·배너 설정·차단 도메인과 현재 `cookie_policy_version`(정책 버전 서비스 기준)을 반환합니다. 차단 도메인은 게스트도 차단이 동작해야 하므로 공개 응답에 노출되며, 이 컨트롤러가 응답 노출의 기준(SSoT)입니다. 조회 전용입니다. + + diff --git a/plugins/_bundled/sirsoft-marketing/docs/api/channels.md b/plugins/_bundled/sirsoft-marketing/docs/api/channels.md new file mode 100644 index 00000000..f2824076 --- /dev/null +++ b/plugins/_bundled/sirsoft-marketing/docs/api/channels.md @@ -0,0 +1,77 @@ +# Channels API 레퍼런스 + +> **소유**: plugin `sirsoft-marketing` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Channels 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### PUT /api/plugins/sirsoft-marketing/admin/channels + +- **라우트명**: `api.plugins.sirsoft-marketing.admin.channels.update` +- **컨트롤러**: `Plugins\Sirsoft\Marketing\Http\Controllers\MarketingAdminController@updateChannels` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** + +관리자 환경설정 화면에서 마케팅 동의 **채널 목록 전체를 한 번에 저장**하는 엔드포인트다. 컨트롤러가 `AdminBaseController` 를 상속하므로 실제 인증은 `auth:sanctum` **에 더해 관리자(admin) 권한**을 요구한다(생성기 표기는 `auth:sanctum` 만 노출). 제출된 배열이 곧 새 상태가 되며, 개별 채널 추가/수정 엔드포인트는 없다(전량 교체 방식). + +**요청 파라미터**는 생성기가 배열 중첩 규칙(`channels.*`)을 평면화하지 못해 위 표에 "없음"으로 표기되나, 실제 `ChannelUpdateRequest` 는 다음 body 를 요구한다: + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| channels | body | array | 예 | 최대 10개 | 저장할 채널 정의 배열(전체 교체) | +| channels.*.key | body | string | 예 | `^[a-z0-9_]+$` | 채널 식별자(소문자·숫자·밑줄) | +| channels.*.label | body | object | 예 | 로케일 맵, 각 값 최대 50자 | 다국어 라벨(`LocaleRequiredTranslatable`) | +| channels.*.page_slug | body | string | 아니오 | 최대 100자 | 연결 약관 페이지 slug | +| channels.*.enabled | body | boolean | 예 | — | 노출 여부 | +| channels.*.is_system | body | boolean | 예 | — | 시스템 채널 플래그 | + +- **시스템 채널 보호**: `is_system=true` 채널은 제출 목록에서 빠져도 서버가 기본 정의를 다시 앞에 끼워 넣어 항상 유지된다. `is_system` 을 true→false 로 위변조하거나 시스템 채널을 누락시키면 검증 오류(422). +- **동의 이력 보호**: 이미 사용자 동의가 존재하는 채널을 목록에서 제거하려 하면 `MarketingConsentService::countConsentedByKey()` 가 0 이 아니어서 422 로 거부된다(개인정보 이력 보존). +- **key 중복 금지**: 같은 `key` 가 둘 이상이면 422. +- 성공 시 저장된 최종 `channels` 배열(시스템 채널 병합 포함)을 `data.channels` 로 되돌려준다. + +**응답 예시** (성공) + +```json +{ + "success": true, + "data": { + "channels": [ + { "key": "email_subscription", "label": { "ko": "광고성 이메일 수신", "en": "Marketing email" }, "page_slug": null, "enabled": true, "is_system": true } + ] + }, + "message": "채널이 저장되었습니다.", + "error": null +} +``` + + diff --git a/plugins/_bundled/sirsoft-marketing/docs/api/settings.md b/plugins/_bundled/sirsoft-marketing/docs/api/settings.md new file mode 100644 index 00000000..5134b7a3 --- /dev/null +++ b/plugins/_bundled/sirsoft-marketing/docs/api/settings.md @@ -0,0 +1,93 @@ +# Settings API 레퍼런스 + +> **소유**: plugin `sirsoft-marketing` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Settings 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/plugins/sirsoft-marketing/settings + +- **라우트명**: `api.plugins.sirsoft-marketing.settings` +- **컨트롤러**: `Plugins\Sirsoft\Marketing\Http\Controllers\MarketingSettingsController@settings` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| marketing_consent_enabled | boolean | `true` | 광고성 정보 수신 전체 동의 축의 노출 여부. false 면 회원가입/마이페이지에서 해당 동의 UI 를 렌더하지 않는다(미설정 시 기본 `true`). | +| marketing_consent_terms_slug | string | `marketing-terms` | 광고성 정보 수신 약관 페이지 라우팅에 쓰는 slug. 미설정 시 `null`. 프론트는 값 자체를 링크 대상에만 사용한다. | +| marketing_consent_terms_slug_set | boolean | `true` | 광고성 정보 수신 약관 slug 존재 여부. 프론트는 이 플래그로 약관 링크 표시 여부를 판정한다. | +| third_party_consent_enabled | boolean | `true` | 제3자 제공 동의 축의 노출 여부. false 면 해당 동의 UI 를 렌더하지 않는다(미설정 시 기본 `false`). | +| third_party_consent_terms_slug | null | `null` | 제3자 제공 약관 페이지 라우팅에 쓰는 slug. 미설정 시 `null`. | +| third_party_consent_terms_slug_set | boolean | `false` | 제3자 제공 약관 slug 존재 여부. 프론트의 약관 링크 표시 판정에 쓴다. | +| info_disclosure_enabled | boolean | `true` | 정보 이용 안내 동의 축의 노출 여부. false 면 해당 안내 UI 를 렌더하지 않는다(미설정 시 기본 `false`). | +| info_disclosure_terms_slug | null | `null` | 정보 이용 안내 약관 페이지 라우팅에 쓰는 slug. 미설정 시 `null`. | +| info_disclosure_terms_slug_set | boolean | `false` | 정보 이용 안내 약관 slug 존재 여부. 프론트의 약관 링크 표시 판정에 쓴다. | +| channels | array | `[{"key":"email_subscription","label":"광고성 이메일 수신","label_…` | `MarketingConsentService::getRegisteredChannels()` 가 반환하는 활성 채널 목록. 각 원소는 `key`·현재 로케일 해석 `label`·로케일 맵 원본 `label_i18n`·`enabled`·`terms_slug`·`terms_slug_set` 를 갖는다. 폼의 반복 렌더링에 그대로 쓰인다. | + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** + +비로그인 상태에서도 조회 가능한 공개 설정 엔드포인트다(`PublicBaseController`). 회원가입 폼과 마이페이지의 마케팅 동의 UI 가, 어떤 동의 항목을 노출할지·약관 링크가 존재하는지·어떤 채널을 반복 렌더링할지 결정하기 위해 소비한다. `plugin_settings` 저장값과 `channels` JSON 을 조합해 반환하며 별도 DB 모델을 두지 않는다. + +- **약관 slug 은 값을 노출하지 않는다.** 각 동의 항목마다 실제 slug(`*_terms_slug`)와 존재 플래그(`*_terms_slug_set`)를 함께 반환하되, slug 미설정 시 값은 `null` 이고 플래그는 `false` 다. 프론트는 링크 표시 여부를 `*_terms_slug_set` 으로 판정하고, slug 값 자체는 약관 페이지 라우팅에만 쓴다. +- **세 가지 법적 동의 축**: `marketing_consent_*`(광고성 정보 수신 전체 동의), `third_party_consent_*`(제3자 제공), `info_disclosure_*`(정보 이용 안내). 각 축의 `*_enabled` 가 false 면 해당 동의 UI 를 렌더하지 않는다. `marketing_consent_enabled` 는 미설정 시 기본 `true`, 나머지 두 축은 기본 `false`. +- **`channels` 배열**은 `MarketingConsentService::getRegisteredChannels()` 가 반환하는 **활성 채널만** 담는다. 각 원소는 `key`, 현재 로케일로 해석된 `label`, 로케일 맵 원본 `label_i18n`, `enabled`, 그리고 slug 값(`terms_slug`)과 존재 플래그(`terms_slug_set`)를 갖는다. 회원가입/마이페이지 폼의 iteration 렌더링에 그대로 쓰인다. +- `label` 은 요청 시점의 `app()->getLocale()` 기준으로 해석되며, 해당 로케일 라벨이 없으면 `fallback_locale`(기본 `ko`) → `key` 순으로 폴백한다. + +**응답 예시** (실측) + +```json +{ + "success": true, + "data": { + "marketing_consent_enabled": true, + "marketing_consent_terms_slug": "marketing-terms", + "marketing_consent_terms_slug_set": true, + "third_party_consent_enabled": true, + "third_party_consent_terms_slug": null, + "third_party_consent_terms_slug_set": false, + "info_disclosure_enabled": true, + "info_disclosure_terms_slug": null, + "info_disclosure_terms_slug_set": false, + "channels": [ + { + "key": "email_subscription", + "label": "광고성 이메일 수신", + "label_i18n": { "ko": "광고성 이메일 수신", "en": "Marketing email" }, + "enabled": true, + "terms_slug": null, + "terms_slug_set": false + } + ] + }, + "message": null, + "error": null +} +``` + + diff --git a/plugins/_bundled/sirsoft-marketing/docs/api/user-consent-injection.md b/plugins/_bundled/sirsoft-marketing/docs/api/user-consent-injection.md new file mode 100644 index 00000000..097548bc --- /dev/null +++ b/plugins/_bundled/sirsoft-marketing/docs/api/user-consent-injection.md @@ -0,0 +1,99 @@ +# 코어 User 응답 마케팅 동의 필드 주입 (Hook Injection) + +> **소유**: plugin `sirsoft-marketing` · 이 문서는 `api:docgen` 생성 대상이 아닌 **훅 주입 계약** 레퍼런스다(플러그인이 코어 응답에 필드를 주입하므로 라우트 단위 문서로 표현되지 않는다). 코어 API 문서(`docs/backend/api/users.md`, `me.md`, `profile.md`)의 응답 필드 표에서 "확장 소유"로 표기된 `*_consent`/`notify_*`/`{channel}_*` 필드의 실제 계약이 여기에 있다. + +--- + +## TL;DR (5초 요약) + +```text +1. sirsoft-marketing 은 core.user.filter_resource_data 훅으로 코어 User 응답에 마케팅 동의 필드를 병합한다 +2. 주입 대상: /me, /auth/user, /admin/users/{user}, 프로필 등 User 리소스를 반환하는 모든 코어 엔드포인트 +3. 주입 필드는 활성 채널·법적 항목·마스터 키에 따라 동적 — 고정 스키마가 아니다 +4. 각 동의 항목마다 상태({key})·시각({key}_at)·활성화 플래그({key}_enabled)·약관 slug 3종을 함께 내린다 +5. 이 필드들이 코어 문서에서 TODO 로 남은 이유: 코어가 아니라 이 플러그인이 소유하기 때문 +``` + +--- + +## 왜 이 문서가 필요한가 + +코어 `UserResource` 등 User 계열 리소스는 `core.user.filter_resource_data` 필터 훅을 발화한다. +`sirsoft-marketing` 플러그인의 `MarketingConsentListener::filterResourceData()` 가 이 훅을 구독해 +**코어 User 응답 `data` 에 마케팅 동의 관련 필드를 병합**한다. 따라서 `docs/backend/api/` 의 코어 User +문서에 나타나는 `marketing_consent`, `third_party_consent_at`, `email_subscription_enabled`, +`channels`, `consent_histories` 같은 필드는 코어 코드에는 없고 이 플러그인이 런타임에 주입한 것이다. + +코어 문서 생성기(`api:docgen`)는 정적 리플렉션으로 코어 Resource `toArray()` 만 읽으므로 이 필드들을 +설명하지 못하고 TODO 로 남긴다. 그 설명의 SSoT 가 이 문서다. + +## 주입 지점 + +`core.user.filter_resource_data` 훅을 발화하는 모든 코어 엔드포인트에서 주입된다. 대표적으로: + +| 엔드포인트 | 문서 | +|---|---| +| `GET /api/me` | `docs/backend/api/me.md` | +| `GET /api/auth/user`, `GET /api/user/auth/user` | `docs/backend/api/auth.md` | +| `GET /api/admin/users/{user}` | `docs/backend/api/users.md` | +| 프로필 조회/수정 응답 | `docs/backend/api/profile.md` | + +## 주입 필드 계약 + +주입 필드 집합은 **활성 채널 목록 + 법적 필수 항목 + 마스터 키**에 따라 동적으로 결정된다. 고정 스키마가 +아니므로 아래는 필드 **패턴**으로 기술한다. + +### 동의 상태 필드 (동의 키별 2개) + +동의 키 집합 = 활성 채널 키(`channels` JSON 의 `enabled` 채널) ∪ 마스터 키 `marketing_consent` +∪ 활성화된 법적 키(`third_party_consent`, `info_disclosure` 중 `*_enabled` 인 것). + +| 필드 | 타입 | 용도/설명 | +|---|---|---| +| `{consent_key}` | boolean | 해당 항목에 동의했는지. 동의 레코드 없으면 `false` | +| `{consent_key}_at` | string\|null | 동의 시각(ISO 8601). 미동의면 `null` | + +- `marketing_consent` (마스터 키): 마케팅 채널 전체 동의/철회를 제어하는 상위 키. +- `third_party_consent`, `info_disclosure`: 법적 필수 동의 항목(고정 키, 활성화 시에만 주입). +- 그 외 키는 관리자가 정의한 동적 채널 키(예: `email_subscription`, `sms_subscription`). + +### 활성화·약관 플래그 필드 (프론트 조건부 렌더링용) + +마스터·법적·채널 각각에 대해 3종 플래그를 내린다. 값 규칙은 공개 `GET /settings` 응답과 동일하다. + +| 필드 패턴 | 타입 | 용도/설명 | +|---|---|---| +| `marketing_consent_enabled` | boolean | 마케팅 전체 동의 UI 노출 여부(기본 `true`) | +| `marketing_consent_terms_slug` | string\|null | 마케팅 약관 slug(미설정 `null`) | +| `marketing_consent_terms_slug_set` | boolean | 마케팅 약관 존재 여부 | +| `third_party_consent_enabled` / `_terms_slug` / `_terms_slug_set` | boolean/string·null/boolean | 제3자 제공 동의 항목 플래그 | +| `info_disclosure_enabled` / `_terms_slug` / `_terms_slug_set` | boolean/string·null/boolean | 정보 이용 안내 동의 항목 플래그 | +| `{channel_key}_enabled` / `_terms_slug` / `_terms_slug_set` | boolean/string·null/boolean | 동적 채널별 노출·약관 플래그(모든 채널에 대해, 활성 여부 무관) | + +> 약관 slug 은 `*_terms_slug`(값)와 `*_terms_slug_set`(존재 플래그)을 함께 내린다. 프론트는 링크 표시를 +> `*_terms_slug_set` 으로 판정한다. 공개 `settings.md` 계약과 동일하다. + +### 컬렉션 필드 + +| 필드 | 타입 | 용도/설명 | +|---|---|---| +| `channels` | array | 관리자 정의 전체 채널 목록(iteration 렌더링용). 원소: `key`, 현재 로케일 `label`, `enabled`, `terms_slug`, `terms_slug_set` | +| `consent_histories` | array | 이 사용자의 동의 변경 이력. 원소: `channel_key`, `action`, `source`(register/admin/profile), `created_at` | + +## 주의사항 + +- **책임 분리**: 이 필드들의 값 규칙·존재 여부는 전적으로 `sirsoft-marketing` 이 결정한다. 플러그인을 + 비활성화하면 코어 User 응답에서 이 필드들이 사라진다. 코어 문서가 이들을 TODO 로 남긴 것은 오류가 + 아니라 책임 분리의 결과다. +- **동적 스키마**: 채널 추가/삭제(`PUT /admin/channels`)나 법적 항목 활성화 설정에 따라 주입 필드 집합이 + 바뀐다. 소비처는 고정 키 목록을 가정하지 말고 `channels` 배열을 순회하는 것을 권장한다. +- **쓰기 경로**: 이 필드들은 조회 시 주입되지만, 저장은 회원가입(`agree_{key}`)·사용자 생성/수정 + (`{key}`) 요청 필드를 같은 리스너가 validation rule 훅으로 추가하고 action 훅에서 별도 저장한다. + User 테이블 컬럼이 아니라 별도 `marketing_consents` EAV 테이블에 보관된다. + +## 관련 + +- [settings.md](settings.md) — 비로그인 공개 설정(동일한 `*_terms_slug_set`·`channels` 계약) +- [channels.md](channels.md) — 채널 목록 저장(동적 채널 키의 출처) +- `docs/backend/api/users.md`, `me.md`, `profile.md`, `auth.md` — 주입 대상 코어 문서 +- `docs/extension/hooks.md` — Filter 훅(`core.user.filter_resource_data`) 계약 diff --git a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/cbt.md b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/cbt.md new file mode 100644 index 00000000..ee835e9d --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/cbt.md @@ -0,0 +1,82 @@ +# Cbt API 레퍼런스 + +> **소유**: plugin `sirsoft-pay_kginicis` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Cbt 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/plugins/sirsoft-pay_kginicis/admin/cbt-connectivity-check + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.cbt.connectivity.check` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminCbtConnectivityCheckController@check` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| egress_ip | string | `118.235.10.131` | 서버가 외부 통신 시 사용하는 egress IP. KG 이니시스 측에 DEVCBT 접근용 IP 화이트리스트 등록을 요청할 때 알려줄 IP이며, 외부 echo 서비스(ipify 등)를 순차 조회해 얻는다(모두 실패 시 null). | +| server_ip | string | `127.0.0.1` | `$_SERVER['SERVER_ADDR']` 로 읽은 서버 내부 IP. egress IP와 대조해 NAT/프록시 여부를 가늠하는 참고값이다. | +| 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 콜백 수신 가능 여부를 점검한다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.read`)이 없는 경우 | + + + +**설명** + +일본 CBT(테스트 모드, DEVCBT) 결제를 위한 호스트 연결 상태를 관리자 설정 페이지에서 셀프 진단하는 엔드포인트입니다. 서버의 egress IP(외부 통신에 쓰이는 IP, KG 이니시스 IP 화이트리스트 등록 대상)와 서버 내부 IP 를 조회하고, `devcbt.inicis.com` 에 대한 DNS 해석 및 TCP 443 도달성(3초 timeout)을 점검하며, 결제 콜백 URL 이 HTTPS·공인 호스트인지 등 콜백 진단 정보도 함께 반환합니다. 운영계(`cbt.inicis.com`)는 IP 화이트리스트 제약이 없어 진단 대상에서 제외됩니다. 관리자 인증(`auth:sanctum`)과 `sirsoft-ecommerce.settings.read` 권한이 필요하며, 토큰 누락·만료는 401, 권한 부족은 403, 진단 중 예외 발생 시 500 으로 응답합니다. + + +### POST /api/plugins/sirsoft-pay_kginicis/admin/cbt-test-product + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.cbt.test-product.create` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminCbtTestProductController@create` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.products.create` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.create`)이 없는 경우 | + + + +**설명** + +일본 CBT(JPPG) 결제 검증을 손쉽게 하기 위해 JPY 가격(100엔)이 설정된 테스트용 상품을 한 번에 자동 생성하는 관리자 엔드포인트입니다. `ProductService::create()` 를 호출해 다국어(ko/en/ja) 이름·설명과 판매 상태, 기본 옵션 1행을 갖춘 상품을 생성하며, 성공 시 `product_id`·`product_code` 와 함께 어드민 편집 URL·일본어 쇼핑몰 URL 을 반환해 운영자가 곧바로 CBT 결제 흐름을 테스트할 수 있게 합니다. 관리자 인증(`auth:sanctum`)과 `sirsoft-ecommerce.products.create` 권한이 필요하며, 토큰 누락·만료는 401, 권한 부족은 403, 상품 생성 중 예외 발생 시 500(에러 상세 포함)으로 응답합니다. + + diff --git a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/orders.md b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/orders.md new file mode 100644 index 00000000..9332bed0 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/orders.md @@ -0,0 +1,505 @@ +# Orders API 레퍼런스 + +> **소유**: plugin `sirsoft-pay_kginicis` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Orders 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/plugins/sirsoft-pay_kginicis/admin/orders/test-mode-map + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.orders.test-mode-map` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminOrderListController@testModeMap` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| 20260619-1358556131 | boolean | `true` | 응답 객체의 키가 곧 주문번호이며 값 `true`는 해당 주문이 KG 이니시스 테스트 모드 결제임을 뜻한다. 목록에 존재하는 주문번호만 테스트 결제로 간주하면 된다. | +| 20260619-1425382147 | boolean | `true` | 위와 동일 — 키가 주문번호, 값 `true`는 테스트 모드 결제 주문임을 나타낸다. | +| APIDOC-KGINICIS-000001 | boolean | `true` | 위와 동일 — 키가 주문번호, 값 `true`는 테스트 모드 결제 주문임을 나타낸다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.read`)이 없는 경우 | + + + +**설명** +`AdminOrderListController@testModeMap` 가 최근 6개월 이내 `pg_provider = kginicis` 결제 주문을 조회해 그중 테스트 모드 결제만 `{ "주문번호": true, ... }` 맵으로 반환한다. 테스트 판별은 `payment_meta.is_test_mode === true` → `pg_raw_response.mid` 가 KG 이니시스 Live MID 접두사(`SIR`)가 아님 → `transaction_id` 에 `Test` 포함 순으로 이루어진다. 어드민 주문 목록에서 결제수단 셀 하단에 "(테스트 결제)" 배지를 붙이는 용도이며, 응답 필드 키가 곧 주문번호이므로 특정 주문이 맵에 존재하면 테스트 결제로 간주하면 된다. `sirsoft-ecommerce.orders.read` 권한이 필요하고, 파라미터 없이 전체 맵을 한 번에 조회한다. + + +### POST /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/cash-receipt + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.orders.cash-receipt.issue` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminCashReceiptController@issue` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** +`AdminCashReceiptController@issue` 가 이미 승인 완료된 KG 이니시스 결제 건에 대해 현금영수증을 별도 발행한다. 요청의 `issue_type`(`0`=소득공제, `1`=지출증빙)과 `issue_number`(휴대폰/사업자번호 등 식별번호)를 검증한 뒤, `paid_amount_local` 기준 금액과 부가세(저장값 우선, 없으면 총액의 10/110)를 계산해 `KgInicisApiService::issueCashReceipt` 로 발행 요청을 보낸다. 발행 성공 시 `is_cash_receipt_issued`, `cash_receipt_type`, 마스킹된 식별번호(`cash_receipt_identifier`, 끝 4자리만 노출)를 저장하며, 식별번호 원문은 저장하지 않는다. `sirsoft-ecommerce.orders.update` 권한이 필요하고, 검증 실패 422 / 주문 미존재 404 / 이미 발행됨 409 / PG 발행 실패(resultCode≠`00`) 502 / 예외 500 으로 상태코드가 매핑된다. + + +### GET /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/cbt-cvs + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.orders.cbt-cvs.show` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminCbtCvsOperationsController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | + +**응답 필드** (`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 | `https://g7_2.dev/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 일시 | +| last_recheck_result | string | `` | 마지막 로컬 상태 재확인 결과. recheck 액션 실행 시 `local_status_checked` 로 기록되며, CBT 편의점 입금은 외부 PG 조회 대상이 아니라 로컬 확인 흔적만 남긴다. | +| expired_at | string | `` | expired 일시 | +| expiry_reason | string | `` | 만료 처리 사유. 입금 기한 경과로 만료된 경우 `payment_term_elapsed` 가 기록된다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** +`AdminCbtCvsOperationsController@show` 가 `CbtCvsOperationsService::summary` 를 호출해 CBT(일본결제) 편의점(CVS) 입금 결제의 운영 요약을 반환한다. 주문번호로 결제·메타(`payment_meta`)를 읽어 입금 상태(`cvs_status`), 편의점 확인번호/접수번호(`conf_no`/`receipt_no`), 입금 마감(`payment_term` 및 포맷된 값), 시간 경과 만료 여부(`is_expired_by_time`), NOTI 통보 이력(`notify_history`), 통보 수신 URL 등을 집계한다. `can_simulate_notify`(테스트 모드 + 입금대기)와 `can_mark_expired`(입금대기 + 기한 경과)는 후속 운영 액션의 버튼 노출을 제어하는 게이트값이다. `sirsoft-ecommerce.orders.read` 권한이 필요하며, 주문이 없으면 플러그인 표준 404(`order_not_found`)를 반환한다. + + +### POST /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/cbt-cvs/expire + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.orders.cbt-cvs.expire` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminCbtCvsOperationsController@expire` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** +`AdminCbtCvsOperationsController@expire` 가 `CbtCvsOperationsService::expireOverdue` 를 호출해 입금 기한이 지난 CBT 편의점 결제를 만료 상태로 전환한다. 결제 row 를 트랜잭션 내에서 잠근 뒤 `canMarkExpired`(CVS 메타 + `waiting_deposit` 상태 + `cvs_payment_term` 경과)를 재판정해 통과할 때만 `payment_status` 를 `expired` 로 바꾸고 메타에 `cvs_expired_at`·`cvs_expiry_reason=payment_term_elapsed` 를 기록한다. 잠금·재판정으로 동시 입금 통보(NOTI)와의 경합을 방지하며, 만료 조건 미충족 시 `not_expirable` 422 를 반환한다. `sirsoft-ecommerce.orders.update` 권한이 필요하고, CVS 결제가 아니면 `not_cvs` 422, 주문 미존재 시 404 로 응답한다. + + +### POST /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/cbt-cvs/recheck + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.orders.cbt-cvs.recheck` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminCbtCvsOperationsController@recheck` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** +`AdminCbtCvsOperationsController@recheck` 가 `CbtCvsOperationsService::markRechecked` 를 호출해 CBT 편의점 결제의 로컬 상태 확인 시각만 메타에 기록한다(`cvs_last_recheck_at`, `cvs_last_recheck_result=local_status_checked`). CBT 편의점 입금은 한국 INIAPI 거래조회 대상이 아니므로 외부 PG 조회 없이 관리자가 "로컬 상태를 확인했다"는 감사 흔적을 남기는 용도이며, 결제 상태 자체는 변경하지 않는다. 처리 후 갱신된 운영 요약(summary)을 반환한다. `sirsoft-ecommerce.orders.read` 권한이 필요하고, CVS 결제가 아니면 `not_cvs` 422, 주문 미존재 시 404 로 응답한다. + + +### POST /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/cbt-cvs/simulate-notify + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.orders.cbt-cvs.simulate-notify` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminCbtCvsOperationsController@simulateNotify` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** +`AdminCbtCvsOperationsController@simulateNotify` 가 `CbtCvsOperationsService::simulatePaidNotify` 를 호출해, 테스트 모드에 한해 편의점 입금 완료 NOTI(`status=00`)를 관리자 동작으로 합성·재생한다. 저장된 메타(`cbt_mid`/`cbt_sid`/`cvs_*`)와 결제 통화 환산 금액으로 payload 를 만들어 실제 통보 처리 경로(`handleNotify`)에 흘려보내므로, 정상 처리 시 결제가 입금완료로 전환되고 주문 결제가 확정된다. 가드로 `is_test_mode` 가 아니면 `not_test_mode` 422, 결제가 `waiting_deposit` 이 아니면 `not_waiting_deposit` 422, 합성 통보가 OK 가 아니면 `simulate_failed` 422 를 반환한다. `sirsoft-ecommerce.orders.update` 권한이 필요하며, 운영(Live) 결제에는 사용할 수 없는 테스트 전용 도구다. + + +### GET /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/cbt-reconciliation + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.orders.cbt-reconciliation.show` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminCbtReconciliationController@show` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | + +**응답 필드** (`data` 내부) + + + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** +`AdminCbtReconciliationController@show` 가 `CbtReconciliationService::get` 을 호출해 주문에 연결된 CBT 자동환불 조정(reconciliation) 레코드를 반환한다. 이 레코드는 CBT 결제의 자동환불 처리 이력(상태, TID, 금액, 환불 결과/에러, 재시도 횟수)을 담으며, `status` 에서 파생된 `manual_action_required`(수동 환불 필요 여부)와 `can_retry`(수동환불필요 + TID 존재 시 재시도 가능)를 함께 노출한다. 조정 레코드가 없는 주문이면 `data` 가 `null` 로 내려간다(정상 응답). `sirsoft-ecommerce.orders.read` 권한이 필요하며, 관리자 화면에서 자동환불 실패 건의 재시도 버튼 활성화 판단에 사용된다. + + +### POST /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/cbt-reconciliation/refund-retry + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.orders.cbt-reconciliation.refund-retry` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminCbtReconciliationController@retryRefund` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** +`AdminCbtReconciliationController@retryRefund` 가 실패한 CBT 자동환불을 관리자 동작으로 재시도한다. 먼저 `claimRefundRetry` 로 레코드를 잠금 획득하는데, `can_retry`(상태가 `manual_refund_required` + TID 존재)가 아니면 획득에 실패해 `not_retryable` 422 를 반환한다. 획득 성공 시 저장된 CBT 자격증명(`is_test_mode`/`cbt_mid`)으로 `KgInicisApiService::refundCbtPayment` 를 호출하고, 성공하면 상태를 `auto_refunded` 로, 예외 발생 시 `manual_refund_required` 로 되돌리며 에러를 기록한다. `sirsoft-ecommerce.orders.update` 권한이 필요하고, 환불 API 실패 시 `retry_failed` 502 로 응답한다. + + +### GET /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/escrow-delivery + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.orders.escrow-delivery.form` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminEscrowDeliveryController@formData` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| has_escrow_payment | boolean | `true` | escrow payment 여부 | +| tid | string | `INIJPGCARDapidocsmpl0000000001` | 배송정보를 등록할 에스크로 결제의 KG 이니시스 거래 ID(transaction_id). INIAPI 배송등록 호출의 대상 거래를 지정한다. | +| price | integer | `170624` | 에스크로 결제 금액(paid_amount_local을 정수 반올림). 배송등록 요청의 price로 전송된다. | +| courier_codes | object | `{"hanjin":"한진택배","cjgls":"CJ대한통운","loge":"롯데택배","epost":"…` | KG 이니시스 공식 택배사 코드표(코드→택배사명). 배송등록 시 `ex_code`는 이 표에 존재하는 코드여야 한다. | +| prefill | object | `{"recvName":"API 문서 샘플 수령인","recvTel":"010-0000-0002","re…` | 배송지 DB에서 채운 수령인 선입력값. 수령인명(`recvName`)·연락처(`recvTel`)·우편번호(`recvPost`)·주소(`recvAddr`)를 담아 등록 폼을 미리 채운다. | +| registered_delivery | null | `null` | 이미 등록된 배송 이력(`payment_meta.escrow_delivery`). 미등록이면 `null`이며, 값이 있으면 운송장·택배사 등 중복 등록 방지 판단에 쓰인다. | +| escrow_confirm | null | `null` | 구매확정 이력(`payment_meta.escrow_confirm`). 구매자 구매확정 처리 전이면 `null`이다. | +| deny_confirmed | boolean | `false` | 판매자 구매거절 확인 이력(`payment_meta.escrow_deny_confirm`) 존재 여부. 이미 거절확인했으면 `true`가 되어 중복 처리를 막는다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** +`AdminEscrowDeliveryController@formData` 가 에스크로 배송등록 폼에 필요한 초기 데이터를 반환한다. 대상 주문의 KG 이니시스 에스크로 결제(`is_escrow = true`)를 찾아 TID, 결제금액, 공식 택배사 코드표(`courier_codes`), 배송지 기반 수령인 선입력값(`prefill`)을 내려주고, `payment_meta` 에서 이미 등록된 배송 이력(`registered_delivery`)·구매확정(`escrow_confirm`)·구매거절확인(`deny_confirmed`) 여부를 함께 제공한다. 에스크로 결제가 아닌 주문은 `has_escrow_payment` 없이 `data` 가 `null` 로 내려간다. `sirsoft-ecommerce.orders.read` 권한이 필요하며, 실제 배송등록(POST) 전 화면 표시·중복 등록 방지 판단에 사용된다. + + +### POST /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/escrow-delivery + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.orders.escrow-delivery.register` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminEscrowDeliveryController@register` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** +`AdminEscrowDeliveryController@register` 가 KG 이니시스 에스크로 결제의 배송정보를 INIAPI 에 등록한다. 운송장번호(`invoice`)와 택배사코드(`ex_code`, 공식 코드표에 존재해야 함)를 필수 검증하고, 수령인 정보는 요청값 우선·부재 시 배송지 DB 로 채운다. 에스크로 결제는 반드시 에스크로 자격증명(`useEscrowCredentials`)으로 `registerEscrowDelivery` 를 호출하며, 성공 시 정제된 PG 응답과 배송 이력을 `payment_meta.escrow_delivery` 에 저장한다. `sirsoft-ecommerce.orders.update` 권한이 필요하고, 입력 검증 실패 422 / 에스크로 결제 미존재 404 / PG 실패(resultCode≠`00`) 502 / 예외 500 으로 매핑된다. + + +### POST /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/escrow-deny-confirm + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.orders.escrow-deny-confirm` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminEscrowDenyConfirmController@confirm` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.update` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** +`AdminEscrowDenyConfirmController@confirm` 이 구매자가 구매거절을 선택한 에스크로 주문에 대해 판매자(관리자) 측 거절 확인을 처리한다(INIAPI v1 `type=Dncf`). 대상 에스크로 결제(`is_escrow = true`)를 찾아 이미 거절확인 이력(`payment_meta.escrow_deny_confirm`)이 있으면 중복 처리 없이 422 로 막고, 에스크로 자격증명으로 `denyConfirmEscrow` 를 호출한다. 성공 시 확인 시각과 담당자명(`dcnf_name`, 기본 "관리자"), 정제된 PG 응답을 메타에 기록한다. `sirsoft-ecommerce.orders.update` 권한이 필요하며, 에스크로 결제 미존재 404 / 이미 확인됨 422 / PG 실패(resultCode≠`00`) 502 / 예외 500 으로 매핑된다. + + +### GET /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/transaction-status + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.orders.transaction-status` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminTransactionController@queryByOrder` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | + +**응답 필드** (`data` 내부) + +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| resultCode | string | `0000` | 거래조회 결과 코드. `queryTransaction` API 응답의 처리 결과 코드이며, CBT 로컬 확인 경로에서는 저장된 승인 코드가 없으면 `LOCAL_CBT` 로 대체된다. | +| resultMsg | string | `정상처리` | 결과 코드에 대응하는 메시지. CBT 로컬 확인 시에는 "CBT 거래는 로컬 결제 확인 정보로 표시됩니다." 안내 문구가 들어간다. | +| tid | string | `INIJPGCARDapidocsmpl0000000001` | 조회 대상 KG 이니시스 거래 ID(transaction_id). | +| _is_cbt | boolean | `true` | 이 거래가 CBT(일본결제)인지 여부. TID가 `INIJPG`로 시작하거나 통화가 JPY이면 CBT로 판정된다. | +| _is_local_confirmation | boolean | `true` | 응답이 실제 INIAPI 조회가 아니라 로컬 저장 정보로 구성됐는지 여부. CBT는 한국 INIAPI 거래조회 대상이 아니라 이 값이 `true`가 된다. | +| _is_test_mode | boolean | `true` | 결제가 테스트 모드였는지 여부. `payment_meta.is_test_mode` 또는 자격증명 모드에서 파생된다. | +| _local_is_escrow | boolean | `true` | 로컬 결제 레코드의 에스크로 결제 여부(`is_escrow`). | +| _pay_method | string | `CVS` | KG 이니시스 결제수단 코드(대문자 정규화). 예: `CARD`(신용카드)·`VBANK`(가상계좌)·`CVS`(일본 편의점결제) 등. | +| _base_pay_method_label | string | `일본 편의점결제` | `_base_pay_method` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| _embedded_pg_provider | null | `null` | 간편결제 등 결제창에 내장된 실제 PG 제공사 식별값(예: `kakaopay`, `naverpay`). 일반 결제면 `null`이다. | +| _embedded_pg_provider_label | null | `null` | `_embedded_pg_provider` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| _pay_method_label | string | `일본 편의점결제` | `_pay_method` 값의 사람이 읽는 라벨 (현지화/Enum 파생) | +| _auth_code | null | `null` | 승인번호. CVS 결제는 확인번호/접수번호를 대체로 사용하며, 카드 등에서는 승인번호(applNum)가 채워진다. | +| _auth_date | string | `2026-07-07 07:17:11` | 승인 일시(YYYY-MM-DD HH:MM:SS 포맷). 승인 일시가 없으면 로컬 결제완료 시각(`paid_at`)으로 대체된다. | +| _total_price | string | `5000` | 결제 총액. 조회 응답 또는 로컬 저장 금액에서 취한 값이다. | +| _currency | string | `JPY` | 결제 통화 코드. CBT는 기본 `JPY`, 일반 국내결제는 원화(응답에 없으면 `WON`)로 표기된다. | +| _moid | string | `APIDOC-KGINICIS-000001` | 가맹점 주문번호(MOID). 이 거래에 대응하는 주문의 주문번호다. | +| _buyer_name | string | `API 문서 샘플 구매자` | 구매자 이름. | +| _buyer_email | string | `apidoc-sample-user@example.com` | 구매자 이메일. | +| _buyer_tel | string | `010-0000-0001` | 구매자 전화번호. | +| _status | string | `waiting_deposit` | 거래/결제 상태. CVS는 결제 상태(`waiting_deposit` 등), 일반 결제는 조회 응답의 거래 상태값이 들어간다. | +| _cancel_price | null | `null` | 취소(환불) 금액. 취소 이력이 없으면 `null`이다. | +| _cancel_date | null | `null` | 취소 일시(YYYY-MM-DD HH:MM:SS). 취소 이력이 없으면 `null`이다. | +| _part_cancel_list | array | `[]` | 부분취소 이력 목록. 각 항목은 취소 금액·일시·메시지·취소 TID로 정규화된다. | +| _card_name | string | `KB국민카드` | 카드 결제 시 발급사(카드사) 이름. | +| _card_num | string | `1554-****-****-9102` | 마스킹된 카드번호(중간 자리는 `*` 처리). | +| _card_code | null | `null` | KG 이니시스 카드사 코드. 없으면 `null`이다. | +| _card_quota | string | `일시불` | 할부 개월. `0`이면 `일시불`, 그 외에는 `N개월`로 변환된다. | +| _card_interest | null | `null` | 무이자 할부 여부. CBT 로컬 확인 경로에서는 `null`이다. | +| _vbank_num | null | `null` | 가상계좌 번호. CVS 결제에서는 확인번호/접수번호로 대체되며, 미해당 시 `null`이다. | +| _vbank_bank_code | null | `null` | 가상계좌 은행 코드. CVS 결제에서는 편의점 식별값(`convenience`)이 들어갈 수 있고, 미해당 시 `null`이다. | +| _vbank_bank_name | string | `CVS` | 가상계좌 은행명. 은행명이 없으면 은행 코드로 매핑하며, 편의점(CVS) 결제에서는 `CVS`로 표기된다. | +| _vbank_holder | null | `null` | 가상계좌 예금주명. 미해당 시 `null`이다. | +| _vbank_expire_date | string | `2026-07-10 23:59:59` | 가상계좌/편의점 입금 마감 일시. 로컬 `vbank_due_at`가 있으면 KST로 변환해 사용하고, 없으면 조회 응답의 입금기한을 포맷한다. | +| _vbank_status | string | `waiting_deposit` | 가상계좌/편의점 입금 상태(입금대기 등). | +| _vbank_paid_at | null | `null` | vbank paid 일시 | +| _bank_code | null | `null` | 계좌이체 은행 코드. 계좌이체 결제가 아니면 `null`이다. | +| _bank_name | null | `null` | 계좌이체 은행명. 은행명이 없으면 은행 코드로 매핑하며, 미해당 시 `null`이다. | +| _bank_acnt_num | null | `null` | 계좌이체 계좌번호. 미해당 시 `null`이다. | +| _hpp_num | null | `null` | 휴대폰 결제 시 결제 휴대폰 번호. 미해당 시 `null`이다. | +| _hpp_corp | null | `null` | 휴대폰 결제 시 이동통신사. 미해당 시 `null`이다. | +| _escrow_status | null | `null` | 에스크로 거래 상태. 에스크로 결제가 아니거나 CBT 로컬 확인 경로에서는 `null`이다. | +| _escrow_confirm | null | `null` | 에스크로 구매확정 일시(YYYY-MM-DD HH:MM:SS). 미확정 시 `null`이다. | +| _inquiry_at | string | `2026-07-07 07:24:51` | inquiry 일시 | +| _local_notice | string | `CBT 거래는 한국 INIAPI 거래조회 대상이 아니므로 저장된 승…` | CBT 로컬 확인 경로에서만 채워지는 안내 문구. 이 거래가 실시간 PG 조회가 아니라 저장된 승인/입금 정보로 표시됨을 관리자에게 알린다. | +| _cbt_cvs | object | `{"status":"waiting_deposit","last_notify_at":"","last_not…` | CBT 편의점(CVS) 결제일 때만 채워지는 입금 운영 요약. 입금 상태·최근 통보/재확인 결과·만료 정보·최근 통보 이력(최대 10건)을 담으며, CVS가 아니면 `null`이다. | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.read`)이 없는 경우 | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** +`AdminTransactionController@queryByOrder` 가 주문번호로 KG 이니시스 결제의 실시간 거래 상태를 조회한다. 결제 TID 를 찾은 뒤, CBT(일본결제, TID 가 `INIJPG` 로 시작하거나 통화 JPY)면 한국 INIAPI 조회 대상이 아니므로 로컬에 저장된 승인/입금 정보로 응답을 구성하고(`_is_local_confirmation=true`), 그 외에는 결제 시점 MID·모드(`is_test_mode`, MID `SIR` 접두사=Live 추정)에 맞는 자격증명으로 실제 `queryTransaction` API 를 호출한다. 응답은 `_pay_method_label`·카드/가상계좌/은행 상세·부분취소 이력·에스크로 상태 등 화면 표시용 `_` 접두 필드로 보강되며, 결제수단/은행/할부 코드는 한국어 라벨로 매핑된다. `sirsoft-ecommerce.orders.read` 권한이 필요하고, 결제 미존재 시 `data`=`null`, 조회 예외 시 502 로 응답한다. + + +### GET /api/plugins/sirsoft-pay_kginicis/user/orders/{orderNumber}/receipt + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.user.orders.receipt` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\UserReceiptController@show` +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | — | 대상 order number의 식별자 | + +**응답 필드** (`data` 내부) + + + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | + + + +**설명** +`UserReceiptController@show` 가 회원/비회원 공용 영수증 조회를 제공한다(선택적 인증). 로그인 사용자는 본인 소유 주문만, 비로그인 사용자는 `X-Guest-Order-Token` 헤더로 비회원 주문을 매칭하고, 토큰이 없거나 stale 하면 결제 직후 PG 콜백이 발급한 단기 영수증 쿠키(5분 유효)로 폴백한다. 소유권/토큰/쿠키 검증 실패는 모두 404 로 통일된다. CBT(일본결제) 결제면 KG 이니시스 매출전표가 없으므로 결제확인서용 필드 목록(`receipt_fields`)과 라벨을 직접 구성해 `receipt_type=cbt_confirmation` 으로 내려주고, 일반 결제는 KG 이니시스 영수증 URL(`receipt_type=inicis_receipt`)을 생성해 반환한다. + + diff --git a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/payment.md b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/payment.md new file mode 100644 index 00000000..d79c0c52 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/payment.md @@ -0,0 +1,169 @@ +# Payment API 레퍼런스 + +> **소유**: plugin `sirsoft-pay_kginicis` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Payment 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### POST /api/plugins/sirsoft-pay_kginicis/payment/cbt/checkout-token + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.payment.cbt.checkout-token` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\CbtCheckoutTokenController@issue` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** + +일본 CBT(국경 간) 결제 흐름의 첫 단계로, 프론트엔드 결제창이 후속 해시 생성 요청에 사용할 단기 체크아웃 토큰을 발급합니다. `CbtCheckoutTokenService::issue()` 가 주문번호·금액·구매자(이메일/전화)·요청 IP·User-Agent 를 HMAC-SHA256 으로 봉인한 서명 토큰을 만들어 반환하며, 이 토큰은 hash-data 단계에서 결제 컨텍스트가 위변조되지 않았는지 검증하는 데 쓰입니다. 인증은 필요 없고(결제창에서 직접 호출), 대신 `oid` 기준 IP별 분당 10회 레이트리밋과 일본 결제 활성화·설정 여부, 주문 존재·결제 가능 상태·통화 JPY 여부·구매자 일치·금액 일치를 순차 검증합니다. 필수 파라미터(`oid`, `price`) 누락 시 422, 레이트리밋 초과 시 429, 주문 미존재 시 404, 구매자 검증 실패 시 403, 그 외 결제 불가 조건은 422 로 응답합니다. + + +### POST /api/plugins/sirsoft-pay_kginicis/payment/cbt/hash-data + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.payment.cbt.hash-data` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\CbtHashDataController@generate` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +_대표 에러 없음 (공개 조회). _ + + + +**설명** + +일본 CBT 결제창이 실제로 KG 이니시스에 전송할 위변조 방지 해시(P_HASHDATA)를 생성해 반환합니다. `KgInicisApiService::generateCbtHashData()` 가 일본 가맹점 MID·타임스탬프·금액·주문번호로 해시를 만들며, 인증은 불필요하지만 checkout-token 단계에서 발급한 토큰을 `CbtCheckoutTokenService::verify()` 로 재검증해 동일한 결제 컨텍스트(주문·금액·구매자·IP·UA)에서 온 요청임을 보장합니다. 재생 공격을 막기 위해 타임스탬프 신선도(`isTimestampFresh`)를 확인하고, `oid` 기준 IP별 분당 10회 레이트리밋과 일본 결제 활성화·설정·주문 상태·통화 JPY·구매자 일치·금액 일치를 검증합니다. 파라미터(`oid`, `price`, `timestamp`) 누락·타임스탬프 만료·금액 불일치 등은 422, 레이트리밋 초과 429, 주문 미존재 404, 구매자 검증 또는 토큰 검증 실패는 403 으로 응답합니다. + + +### POST /api/plugins/sirsoft-pay_kginicis/payment/close-report + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.payment.close-report` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\PaymentCloseReportController@store` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| oid | body | string | 예 | max 40 | 결제창을 닫은 대상 주문의 주문번호. 서버가 이 값으로 주문을 조회해 결제 실패/취소 이력을 기록한다. | +| price | body | integer | 예 | min 1 | 주문 결제 금액. 저장된 주문 청구액과 일치하는지 검증해 위변조된 닫힘 보고를 차단한다. | +| buyer_email | body | string | 아니오 | max 255 | 구매자 이메일. 제공 시 주문의 구매자 정보와 대조해 본인 요청인지 확인하는 데 사용된다. | +| buyer_phone | body | string | 아니오 | max 30 | 구매자 전화번호. 제공 시 주문의 구매자 정보와 대조해 본인 요청인지 확인하는 데 사용된다. | +| payment_method | body | string | 아니오 | max 50 | 사용자가 결제창에서 선택했던 간편결제 등 결제수단 식별값. 결제 메타에 병합해 어떤 수단에서 창을 닫았는지 남긴다. | +| reason | body | string | 아니오 | max 80 | 결제창 닫힘 사유 문자열. 실패/취소 이력에 참고 정보로 기록된다. | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +PC 표준결제창(KRW)에서 사용자가 결제를 완료하지 않고 창을 닫았을 때 프론트엔드가 이를 서버에 보고하는 엔드포인트로, 해당 주문의 결제 실패/취소 이력을 기록합니다. `OrderProcessingService::failPayment()` 로 주문을 `USER_CANCEL` 사유로 실패 처리하고 `recordPaymentCancellation()` 으로 취소 이력을 남기며, 간편결제 선택 정보가 있으면 결제 메타에 병합합니다. 인증은 불필요하나(결제창 컨텍스트에서 호출) FormRequest 검증과 `oid` 기준 IP별 분당 20회 레이트리밋, 주문 존재·통화 KRW·구매자 일치·금액 일치를 검증하고, 이미 결제 가능 상태가 아니거나(`order_not_payable`) 이미 결제 완료(`payment_already_paid`)면 성공 응답에 `status: ignored` 로 무시 처리해 결제 성공 콜백과의 경쟁 상태를 차단합니다. 검증 규칙 위반 시 422, 레이트리밋 초과 429, 주문 미존재 404, 구매자 검증 실패 403 으로 응답합니다. + + +### POST /api/plugins/sirsoft-pay_kginicis/payment/mobile/signature + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.payment.mobile.signature` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\MobileSignatureController@generate` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| oid | body | string | 예 | max 40 | 결제 대상 주문의 주문번호. 서버가 주문을 조회해 결제 가능 상태·통화·금액을 검증하고 이 값을 해시 생성 입력으로 사용한다. | +| price | body | integer | 예 | min 1 | 모바일 결제 금액. 저장된 주문 청구액과 일치하는지 검증한 뒤 P_CHKFAKE 해시 생성에 반영한다. | +| timestamp | body | string | 예 | max 20 | 결제창이 생성한 요청 타임스탬프. 재생 공격 방지를 위해 신선도(만료 여부)를 확인하고 해시 계산에 함께 사용한다. | +| buyer_email | body | string | 아니오 | max 255 | 구매자 이메일. 제공 시 주문의 구매자 정보와 대조해 본인 결제 요청인지 확인하는 데 사용된다. | +| buyer_phone | body | string | 아니오 | max 30 | 구매자 전화번호. 제공 시 주문의 구매자 정보와 대조해 본인 결제 요청인지 확인하는 데 사용된다. | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +모바일 KRW 결제창이 요구하는 위변조 방지 해시(P_CHKFAKE)와 모바일 결제 URL 을 생성해 반환합니다. `KgInicisApiService::generateMobileChkfake()` 로 주문번호·금액·타임스탬프 기반 해시를 만들고 `getMobilePaymentUrl()` 로 결제창 진입 URL 을 함께 내려줍니다. 인증은 불필요하지만(결제창에서 직접 호출) FormRequest 검증에 더해 재생 공격 방지를 위한 타임스탬프 신선도 확인, `oid` 기준 IP별 분당 20회 레이트리밋, 그리고 주문 존재·결제 가능 상태·통화 KRW·구매자 일치·금액 일치를 검증하며 모바일 결제 자격증명 설정 여부도 확인합니다. 파라미터 검증 실패·타임스탬프 만료·통화 불일치·금액 불일치·자격증명 미설정은 422, 레이트리밋 초과 429, 주문 미존재 404, 구매자 검증 실패 403 으로 응답합니다. + + +### POST /api/plugins/sirsoft-pay_kginicis/payment/signature + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.payment.signature` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\PaymentSignatureController@generate` +- **인증/권한**: 공개 (인증 불필요) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| oid | body | string | 예 | max 40 | 결제 대상 주문의 주문번호. 서버가 주문을 조회해 결제 가능 상태·통화·금액을 검증하고 이 값을 서명 생성 입력으로 사용한다. | +| price | body | integer | 예 | min 100 | PC 표준결제창 결제 금액(최소 100원). 저장된 주문 청구액과 일치하는지 검증한 뒤 signature 생성에 반영한다. | +| timestamp | body | string | 예 | max 20 | 결제창이 생성한 요청 타임스탬프. 재생 공격 방지를 위해 신선도(만료 여부)를 확인하고 서명 계산에 함께 사용한다. | +| buyer_email | body | string | 아니오 | max 255 | 구매자 이메일. 제공 시 주문의 구매자 정보와 대조해 본인 결제 요청인지 확인하는 데 사용된다. | +| buyer_phone | body | string | 아니오 | max 30 | 구매자 전화번호. 제공 시 주문의 구매자 정보와 대조해 본인 결제 요청인지 확인하는 데 사용된다. | + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | + + + +**설명** + +PC 표준결제창(KRW)이 요구하는 서명(signature)·검증값(verification)·mKey 를 생성해 반환하는, PC 결제 흐름의 시작점입니다. `KgInicisApiService` 의 `generateSignature()`·`generateVerification()`·`getMKey()` 를 호출해 주문번호·금액·타임스탬프로 만든 서명 세트를 내려주며, 프론트엔드는 이 값으로 KG 이니시스 표준결제창을 호출합니다. 인증은 불필요하나(결제창에서 직접 호출) FormRequest 검증, 재생 공격 방지용 타임스탬프 신선도 확인, `oid` 기준 IP별 분당 20회 레이트리밋, 주문 존재·결제 가능 상태·통화 KRW·구매자 일치·금액 일치 검증과 표준결제 자격증명 설정 여부를 확인합니다. 파라미터 검증 실패·타임스탬프 만료·통화/금액 불일치·자격증명 미설정은 422, 레이트리밋 초과 429, 주문 미존재 404, 구매자 검증 실패 403 으로 응답합니다. + + diff --git a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/transaction.md b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/transaction.md new file mode 100644 index 00000000..0e32c318 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/transaction.md @@ -0,0 +1,47 @@ +# Transaction API 레퍼런스 + +> **소유**: plugin `sirsoft-pay_kginicis` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Transaction 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### POST /api/plugins/sirsoft-pay_kginicis/admin/transaction/query + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.transaction.query` +- **컨트롤러**: `Plugins\Sirsoft\PayKginicis\Controllers\AdminTransactionController@query` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.orders.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.read`)이 없는 경우 | + + + +**설명** + +거래번호(TID)를 직접 입력받아 KG 이니시스 거래 상태를 조회하고 화면 표시용으로 보강해 반환하는 관리자 엔드포인트입니다. 로컬 결제 레코드(`ecommerce_order_payments`)에서 결제 시점 MID·테스트 모드·에스크로 여부를 해석해 해당 자격증명으로 `KgInicisApiService::queryTransaction()` 을 호출하며, 응답을 카드/가상계좌/간편결제/취소 이력 등 상세 필드로 정규화하고 은행 코드→은행명·할부 개월·날짜 포맷을 변환합니다. 일본 CBT 거래(TID `INIJPG` prefix 또는 통화 JPY)는 한국 INIAPI 조회 대상이 아니므로 저장된 로컬 승인/입금 확인 정보로 결과를 구성합니다. 관리자 인증(`auth:sanctum`)과 `sirsoft-ecommerce.orders.read` 권한이 필요하며, `tid` 미입력은 422, 토큰 누락·만료 401, 권한 부족 403, KG 이니시스 조회 실패 시 502 로 응답합니다. + + diff --git a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/vbank.md b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/vbank.md new file mode 100644 index 00000000..10e19d51 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/vbank.md @@ -0,0 +1,51 @@ +# Vbank API 레퍼런스 + +> **소유**: plugin `sirsoft-pay_kginicis` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Vbank 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/plugins/sirsoft-pay_kginicis/admin/vbank-notify-url + +- **라우트명**: `api.plugins.sirsoft-pay_kginicis.admin.vbank.notify.url` +- **인증/권한**: `auth:sanctum` + `permission:sirsoft-ecommerce.settings.read` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`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 | + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.read`)이 없는 경우 | + + + +**설명** + +가상계좌 입금통보를 받을 콜백 URL(PC 웹용 `url`, 모바일용 `mobile_url`)을 관리자 설정 페이지에 표시하기 위해 반환하는 엔드포인트입니다. 라우트 파일 내 클로저로 정의되어 있으며, `url()` 헬퍼로 현재 사이트 도메인 기준의 절대 URL(`/plugins/sirsoft-pay_kginicis/payment/vbank-notify` 및 `.../payment/mobile/vbank-notify`)을 조합해 내려줍니다. 운영자는 이 URL 을 KG 이니시스 가맹점 설정에 입금통보 URL 로 등록해, 구매자가 가상계좌에 실제로 입금했을 때 KG 이니시스가 이 주소로 통보를 보내도록 합니다. 관리자 인증(`auth:sanctum`)과 `sirsoft-ecommerce.settings.read` 권한이 필요하며, 토큰 누락·만료는 401, 권한 부족은 403 으로 응답합니다. + + diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Support/ApiDoc/ApiDocSampleService.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Support/ApiDoc/ApiDocSampleService.php new file mode 100644 index 00000000..660637fe --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Support/ApiDoc/ApiDocSampleService.php @@ -0,0 +1,222 @@ +}> 도메인 => 대표 레코드 정보 + */ + public function seed(): array + { + // ecommerce 모델이 없으면(모듈 비활성) 실측 폴백 없이 빈 맵 반환. + if (! class_exists(Order::class)) { + return []; + } + + $actor = $this->sampleActor(); + if ($actor === null) { + return []; + } + + $order = $this->seedOrder($actor); + $this->seedPayment($order); + $this->seedShippingAddress($order); + + // orders/{orderNumber} 계열 GET(cbt-cvs·cbt-reconciliation·escrow-delivery· + // transaction-status)과 user/orders/{orderNumber}/receipt 를 실측하기 위한 + // path 파라미터 맵. route-model binding 이 없는 문자열 param 을 실제 주문번호로 + // 정확 일치 치환한다. + $orderParams = [ + 'orderNumber' => (string) $order->order_number, + ]; + + $entry = [ + 'model' => Order::class, + 'key' => 'order_number', + 'value' => (string) $order->order_number, + 'path_params' => $orderParams, + ]; + + // 라우트 도메인 그룹(orders.md · vbank.md · payment.md · cbt.md · transaction.md)이 + // 공유하는 {orderNumber} 를 모두 실측 가능하도록 각 그룹에 동일 맵을 노출한다. + return [ + 'orders' => $entry, + 'vbank' => $entry, + 'transaction' => $entry, + 'cbt' => $entry, + 'payment' => $entry, + ]; + } + + /** + * 실측 사용자 소유의 완전 샘플 kginicis 주문을 멱등 생성합니다. + * + * @param User $actor 실측 사용자 + * @return Order 대표 주문 레코드 + */ + private function seedOrder(User $actor): Order + { + $order = Order::query()->where('order_number', self::SAMPLE_ORDER_NUMBER)->first(); + if ($order) { + return $order; + } + + return Order::factory()->forUser($actor)->create([ + 'order_number' => self::SAMPLE_ORDER_NUMBER, + ]); + } + + /** + * 완전 샘플 kginicis 결제(에스크로 + CBT 편의점 메타)를 멱등 생성합니다. + * + * 채우는 특화 필드: + * - pg_provider = 'kginicis' (모든 조회 컨트롤러의 WHERE 조건) + * - is_escrow = true (escrow-delivery formData 의 findEscrowPayment 조건) + * - payment_meta.is_cbt = true + pay_method = 'CVS' (cbt-cvs summary 의 is_cbt_cvs 판정) + * - transaction_id (모든 조회 컨트롤러가 요구하는 non-null TID) + * + * @param Order $order 대표 주문 + * @return OrderPayment 대표 결제 레코드 + */ + private function seedPayment(Order $order): OrderPayment + { + $payment = OrderPayment::query() + ->where('order_id', $order->id) + ->where('pg_provider', 'kginicis') + ->first(); + + if ($payment) { + return $payment; + } + + return OrderPayment::factory()->forOrder($order)->create([ + 'pg_provider' => 'kginicis', + 'transaction_id' => self::SAMPLE_TID, + 'merchant_order_id' => 'MO-'.self::SAMPLE_ORDER_NUMBER, + 'payment_status' => PaymentStatusEnum::PAID, + 'payment_method' => PaymentMethodEnum::VBANK, + 'is_escrow' => true, + 'buyer_name' => 'API 문서 샘플 구매자', + 'buyer_email' => 'apidoc-sample-user@example.com', + 'buyer_phone' => '010-0000-0001', + 'payment_name' => 'API 문서 샘플 CBT 결제', + 'payment_meta' => [ + 'mid' => 'apidocmid1', + 'is_test_mode' => true, + 'is_cbt' => true, + 'cbt_type' => 'cvs', + 'pay_method' => 'CVS', + 'cbt_mid' => 'apidocmid1', + 'cbt_sid' => 'apidocsid1', + 'cvs_convenience' => 'seven_eleven', + 'cvs_conf_no' => '1234567890', + 'cvs_receipt_no' => '0987654321', + 'cvs_amount' => 5000, + 'cvs_status' => 'waiting_deposit', + 'cvs_payment_term' => '20260710235959', + 'cvs_last_notify_at' => '', + 'cvs_notify_history' => [], + 'pg_raw_response' => [ + 'resultCode' => '0000', + 'resultMsg' => '정상처리', + 'tid' => self::SAMPLE_TID, + 'payMethod' => 'CVS', + 'currency' => 'JPY', + ], + ], + ]); + } + + /** + * escrow-delivery formData 의 배송지 prefill 을 실측하기 위한 배송지를 멱등 생성합니다. + * + * @param Order $order 대표 주문 + */ + private function seedShippingAddress(Order $order): void + { + $exists = DB::table('ecommerce_order_addresses') + ->where('order_id', $order->id) + ->where('address_type', 'shipping') + ->exists(); + + if ($exists) { + return; + } + + DB::table('ecommerce_order_addresses')->insert([ + 'order_id' => $order->id, + 'address_type' => 'shipping', + 'orderer_name' => 'API 문서 샘플 주문자', + 'orderer_phone' => '010-0000-0001', + 'orderer_email' => 'apidoc-sample-user@example.com', + 'recipient_name' => 'API 문서 샘플 수령인', + 'recipient_phone' => '010-0000-0002', + 'zipcode' => '06134', + 'address' => '서울특별시 강남구 테헤란로 001', + 'address_detail' => 'API 문서 샘플 빌딩 1층', + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + /** + * 샘플 주문 소유자로 쓸 사용자를 반환합니다. + * + * 코어 완전 샘플 사용자(admin role, docgen 실측 주체)를 우선하고, + * 없으면 첫 사용자로 폴백합니다. + * + * @return User|null 샘플 사용자 (없으면 null) + */ + private function sampleActor(): ?User + { + return User::query()->where('email', 'apidoc-sample-user@example.com')->first() + ?? User::query()->orderBy('id')->first(); + } +} diff --git a/plugins/_bundled/sirsoft-pay_kginicis/tests/Unit/Support/ApiDocSampleServiceTest.php b/plugins/_bundled/sirsoft-pay_kginicis/tests/Unit/Support/ApiDocSampleServiceTest.php new file mode 100644 index 00000000..d3c6e823 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_kginicis/tests/Unit/Support/ApiDocSampleServiceTest.php @@ -0,0 +1,144 @@ +create([ + 'email' => 'apidoc-sample-user@example.com', + 'name' => 'API 문서 샘플 사용자', + ]); + } + + /** + * 시더가 계약을 구현한다. + */ + public function test_seeder_implements_contract(): void + { + $this->assertInstanceOf(ApiDocSampleSeeder::class, new ApiDocSampleService); + } + + /** + * 시더가 kginicis 결제 도메인 대표 샘플을 멱등 생성한다. + */ + public function test_seed_creates_kginicis_escrow_cbt_order(): void + { + (new ApiDocSampleService)->seed(); + + $order = Order::query()->where('order_number', self::SAMPLE_ORDER_NUMBER)->first(); + $this->assertNotNull($order, '샘플 kginicis 주문이 생성되어야 한다'); + + $payment = OrderPayment::query() + ->where('order_id', $order->id) + ->where('pg_provider', 'kginicis') + ->first(); + $this->assertNotNull($payment, 'kginicis 결제 레코드가 생성되어야 한다'); + + // escrow-delivery formData 의 findEscrowPayment 조건 + $this->assertTrue((bool) $payment->is_escrow, '에스크로 결제여야 한다'); + $this->assertNotEmpty($payment->transaction_id, 'TID 가 존재해야 한다'); + + // cbt-cvs summary 의 isCbtCvsMeta 판정 조건 (is_cbt=true + pay_method=CVS) + $meta = is_array($payment->payment_meta) ? $payment->payment_meta : []; + $this->assertTrue(($meta['is_cbt'] ?? false) === true, 'CBT 결제 메타여야 한다'); + $this->assertSame('CVS', strtoupper((string) ($meta['pay_method'] ?? '')), 'CVS 결제수단이어야 한다'); + + // escrow-delivery 배송지 prefill 실측용 shipping 주소 + $hasShipping = DB::table('ecommerce_order_addresses') + ->where('order_id', $order->id) + ->where('address_type', 'shipping') + ->exists(); + $this->assertTrue($hasShipping, '배송지가 생성되어야 한다'); + } + + /** + * 시더가 orders/{orderNumber} 실측용 path_params 맵을 도메인 그룹별로 반환한다. + */ + public function test_seed_returns_path_params_map_for_order_domains(): void + { + $map = (new ApiDocSampleService)->seed(); + + // 5개 라우트 도메인 그룹이 {orderNumber} 를 공유 + foreach (['orders', 'vbank', 'transaction', 'cbt', 'payment'] as $domain) { + $this->assertArrayHasKey($domain, $map, "{$domain} 도메인 맵이 있어야 한다"); + $this->assertSame('order_number', $map[$domain]['key']); + $this->assertSame(self::SAMPLE_ORDER_NUMBER, $map[$domain]['value']); + $this->assertArrayHasKey('path_params', $map[$domain]); + $this->assertSame( + self::SAMPLE_ORDER_NUMBER, + $map[$domain]['path_params']['orderNumber'], + "{$domain} 의 orderNumber 치환값이 실제 주문번호여야 한다", + ); + } + } + + /** + * 시더는 멱등하다 — 재실행 시 중복 레코드를 만들지 않는다. + */ + public function test_seed_is_idempotent(): void + { + $service = new ApiDocSampleService; + $service->seed(); + $service->seed(); + + $orderCount = Order::query()->where('order_number', self::SAMPLE_ORDER_NUMBER)->count(); + $this->assertSame(1, $orderCount, '주문은 1건만 존재해야 한다'); + + $order = Order::query()->where('order_number', self::SAMPLE_ORDER_NUMBER)->first(); + $paymentCount = OrderPayment::query() + ->where('order_id', $order->id) + ->where('pg_provider', 'kginicis') + ->count(); + $this->assertSame(1, $paymentCount, 'kginicis 결제는 1건만 존재해야 한다'); + + $addressCount = DB::table('ecommerce_order_addresses') + ->where('order_id', $order->id) + ->where('address_type', 'shipping') + ->count(); + $this->assertSame(1, $addressCount, '배송지는 1건만 존재해야 한다'); + } + + /** + * 샘플 사용자가 없으면(코어 샘플/기존 사용자 부재) 빈 맵을 반환한다. + */ + public function test_seed_returns_empty_map_when_no_user(): void + { + // RefreshDatabase + 시더로 생성된 사용자를 모두 제거해 폴백 경로를 검증. + User::query()->delete(); + + $map = (new ApiDocSampleService)->seed(); + + $this->assertSame([], $map, '사용자가 없으면 빈 맵이어야 한다'); + } +} diff --git a/plugins/_bundled/sirsoft-verification_kginicis/docs/api/identity.md b/plugins/_bundled/sirsoft-verification_kginicis/docs/api/identity.md new file mode 100644 index 00000000..72ef97d5 --- /dev/null +++ b/plugins/_bundled/sirsoft-verification_kginicis/docs/api/identity.md @@ -0,0 +1,96 @@ +# Identity API 레퍼런스 + +> **소유**: plugin `sirsoft-verification_kginicis` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 실제 API 호출로 실측한 Identity 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표 +3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다 +4. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +5. 설명(TODO) 칸은 사람이 채웁니다 +``` + +--- + + +### GET /api/plugins/sirsoft-verification_kginicis/me/identity/inicis + +- **라우트명**: `api.plugins.sirsoft-verification_kginicis.me.identity.inicis.show` +- **컨트롤러**: `\Plugins\Sirsoft\VerificationKginicis\Http\Controllers\MyInicisIdentityShowController@show` +- **인증/권한**: `auth:sanctum` + +**요청 파라미터** + +_요청 파라미터 없음._ + +**응답 필드** (`data` 내부) + + + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | + + + +**설명** + +로그인한 사용자가 마이페이지 본인인증 카드에서 자신의 KG이니시스 본인확인 정보(마스킹)를 조회하는 엔드포인트다. `AuthBaseController` 를 상속하므로 `auth:sanctum` 인증이 필수이며, 라우트에 `check.user_status:active` 미들웨어가 걸려 **활성 상태 사용자만** 호출할 수 있다(정지/탈퇴 대기 사용자는 차단). 조회 전용이며 부수 효과는 없다. + +- **요청 파라미터 없음**: 대상은 항상 인증된 본인(`Auth::id()`)이며, 다른 사용자의 정보를 조회할 수 없다. 컨트롤러가 `InicisIdentityCardService::findForUser(Auth::id())` 로 본인 record 만 조회한다. +- **미인증 사용자(record 없음)**: 아직 본인인증을 하지 않은 사용자는 `data: null` 로 응답한다(코어 표준 null 처리 — HTTP 200, `success: true`). 프론트는 `data` 가 `null` 인 경우 「본인인증 하기」 유도 UI 를, 값이 있으면 본인확인 카드를 렌더한다. +- **PII 마스킹**: 평문 개인정보는 서버에서 마스킹 후 노출한다(PIPC 사용자 본인 PII 열람권 충족). `di`/`ci` 등 식별값은 **일체 노출하지 않는다**. 이름은 첫 글자만(`홍**`), 생년월일은 연도만(`1990-**-**`), 휴대폰은 앞 3자리+뒤 4자리(`010-****-5678`)로 마스킹된다. +- **응답 필드** (`data` 내부, record 존재 시 — `InicisIdentityResource`): + + | 필드 | 타입 | 예시값 | 용도/설명 | + | --- | --- | --- | --- | + | method | string | `"KG이니시스 본인확인"` | 본인확인 수단 표시 라벨(고정 문자열). | + | verified_at | string\|null | `"2026-07-01 14:22:10"` | 최종 본인확인 시각(`Y-m-d H:i:s`). 재인증이 있었으면 `re_verified_at`, 없으면 최초 `verified_at`. | + | name_masked | string | `"홍**"` | 마스킹된 실명(첫 글자 + 나머지 `*`). | + | birthday_masked | string | `"1990-**-**"` | 마스킹된 생년월일(연도만 노출). | + | phone_masked | string | `"010-****-5678"` | 마스킹된 휴대폰 번호(앞 3 + 뒤 4자리). | + | is_adult | boolean | `true` | 성인 여부(연령 게이팅 판정에 사용). | + | is_foreigner | boolean | `false` | 외국인 여부. | + + 이 외에 `BaseApiResource` 공통 메타 `is_owner`(항상 본인이므로 `true`) + `abilities` 가 함께 붙는다. +- **미설치 주의**: 이 문서는 플러그인 미설치 상태의 라우트 정적 분석으로 생성되어 실측 응답이 없다(`no-token`). 설치 후 `php artisan api:docgen --scope=plugin:sirsoft-verification_kginicis --seed` 로 실측하면 응답 예시가 채워진다. + +**응답 예시** (record 존재 시 — 정적, `InicisIdentityResource` 구조 기준) + +```json +{ + "success": true, + "data": { + "method": "KG이니시스 본인확인", + "verified_at": "2026-07-01 14:22:10", + "name_masked": "홍**", + "birthday_masked": "1990-**-**", + "phone_masked": "010-****-5678", + "is_adult": true, + "is_foreigner": false, + "is_owner": true, + "abilities": {} + }, + "message": "성공적으로 처리되었습니다.", + "error": null +} +``` + +**응답 예시** (본인인증 이력 없음 — record 없음) + +```json +{ + "success": true, + "data": null, + "message": "성공적으로 처리되었습니다.", + "error": null +} +``` + + diff --git a/resources/views/dev-dashboard.blade.php b/resources/views/dev-dashboard.blade.php index cee4640c..aa000a9e 100644 --- a/resources/views/dev-dashboard.blade.php +++ b/resources/views/dev-dashboard.blade.php @@ -855,6 +855,24 @@ if (isset($_GET['ajax_action'])) { + +
+
+ 📘 + API 문서 +
+
+ + +
+
+
diff --git a/tests/Feature/Console/ApiDocSampleServiceTest.php b/tests/Feature/Console/ApiDocSampleServiceTest.php new file mode 100644 index 00000000..86897ce3 --- /dev/null +++ b/tests/Feature/Console/ApiDocSampleServiceTest.php @@ -0,0 +1,67 @@ +seed(); + + $this->assertArrayHasKey('users', $map); + $this->assertArrayHasKey('roles', $map); + $this->assertArrayHasKey('menus', $map); + $this->assertArrayHasKey('schedules', $map); + + // 각 항목은 model/key/value 구조 + $this->assertSame(User::class, $map['users']['model']); + $this->assertArrayHasKey('key', $map['users']); + $this->assertNotEmpty($map['users']['value']); + } + + #[Test] + public function 완전_사용자_샘플은_프로필_필드가_채워진다(): void + { + (new ApiDocSampleService)->seed(); + + $user = User::where('email', 'apidoc-sample-user@example.com')->first(); + + $this->assertNotNull($user); + $this->assertNotNull($user->nickname); + $this->assertNotNull($user->mobile); + $this->assertSame('KR', $user->country); + $this->assertSame('ko', $user->language); + $this->assertNotNull($user->bio); + $this->assertGreaterThan(0, $user->roles()->count()); + } + + #[Test] + public function 재실행_시_샘플이_중복_생성되지_않는다(): void + { + $service = new ApiDocSampleService; + + $service->seed(); + $countAfterFirst = User::where('email', 'apidoc-sample-user@example.com')->count(); + + $service->seed(); + $countAfterSecond = User::where('email', 'apidoc-sample-user@example.com')->count(); + + $this->assertSame(1, $countAfterFirst); + $this->assertSame(1, $countAfterSecond); + } +} diff --git a/tests/Unit/Console/ApiDocBackfillFieldsCommandTest.php b/tests/Unit/Console/ApiDocBackfillFieldsCommandTest.php new file mode 100644 index 00000000..efac5564 --- /dev/null +++ b/tests/Unit/Console/ApiDocBackfillFieldsCommandTest.php @@ -0,0 +1,145 @@ +` 셀만 + * 리소스 계약 사전 설명으로 치환되는지(파라미터 표·실측 예시값·타입은 불변) 검증한다. + * 재실행 멱등성과 도메인 특이 필드 TODO 유지도 확인한다. + */ +class ApiDocBackfillFieldsCommandTest extends TestCase +{ + /** + * 커맨드의 backfillFile private 메서드를 리플렉션으로 호출합니다. + * + * @param string $content 문서 내용 + * @return array{result: string, filled: int, skipped: int} 치환 결과와 카운트 + */ + private function backfill(string $content): array + { + $command = app(ApiDocBackfillFieldsCommand::class); + $ref = new ReflectionMethod($command, 'backfillFile'); + $ref->setAccessible(true); + + $describer = new ResourceFieldDescriber; + $filled = 0; + $skipped = 0; + $result = $ref->invokeArgs($command, [$content, $describer, &$filled, &$skipped]); + + return ['result' => $result, 'filled' => $filled, 'skipped' => $skipped]; + } + + /** + * 응답 필드 표 뼈대를 만듭니다. + * + * @param string $rows 표 행들 + * @return string 마크다운 + */ + private function fieldTable(string $rows): string + { + return "**응답 필드** (`data` 내부)\n\n" + ."| 필드 | 타입 | 실측 예시값 | 용도/설명 |\n" + ."| --- | --- | --- | --- |\n" + .$rows."\n"; + } + + #[Test] + public function 응답_필드_표의_todo_를_계약_설명으로_채운다(): void + { + $content = $this->fieldTable( + "| created_at | string | `2026-07-07` | |\n" + .'| is_owner | boolean | `true` | |' + ); + + $out = $this->backfill($content); + + $this->assertStringContainsString('생성 일시', $out['result']); + $this->assertStringContainsString('소유자', $out['result']); + $this->assertStringNotContainsString('', $out['result']); + $this->assertSame(2, $out['filled']); + $this->assertSame(0, $out['skipped']); + } + + #[Test] + public function 실측_예시값과_타입은_불변이다(): void + { + $content = $this->fieldTable( + '| created_at | string | `2026-07-07 05:13:51` | |' + ); + + $out = $this->backfill($content); + + // 실측 예시값 셀과 타입 셀이 그대로 유지되어야 한다 + $this->assertStringContainsString('| created_at | string | `2026-07-07 05:13:51` |', $out['result']); + } + + #[Test] + public function 파라미터_표의_todo_는_건드리지_않는다(): void + { + // 응답 표 헤더가 아니므로 진입하지 않아야 한다 (파라미터 TODO 마커도 다름) + $content = "**요청 파라미터**\n\n" + ."| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |\n" + ."| --- | --- | --- | --- | --- | --- |\n" + ."| page | query | integer | 아니오 | — | |\n"; + + $out = $this->backfill($content); + + $this->assertSame($content, $out['result']); + $this->assertStringContainsString('', $out['result']); + $this->assertSame(0, $out['filled']); + } + + #[Test] + public function 도메인_특이_필드는_todo_를_유지한다(): void + { + $content = $this->fieldTable( + "| purpose | string | `login` | |\n" + .'| channels | array | `[]` | |' + ); + + $out = $this->backfill($content); + + // ResourceFieldDescriber 가 null 반환 → TODO 유지 + $this->assertSame(2, substr_count($out['result'], '')); + $this->assertSame(0, $out['filled']); + $this->assertSame(2, $out['skipped']); + } + + #[Test] + public function 두번_실행해도_결과가_동일하다_멱등(): void + { + $content = $this->fieldTable( + "| created_at | string | `2026-07-07` | |\n" + .'| purpose | string | `login` | |' + ); + + $first = $this->backfill($content)['result']; + $second = $this->backfill($first)['result']; + + $this->assertSame($first, $second); + // 이미 채워진 셀은 다시 채우지 않음 (도메인 특이만 skipped 로 카운트) + $this->assertSame(0, $this->backfill($first)['filled']); + } + + #[Test] + public function 표_밖의_todo_는_무시한다(): void + { + // 응답 필드 표가 끝난 뒤(사람 서술)의 동일 마커는 치환 대상이 아니다 + $content = $this->fieldTable( + '| created_at | string | `2026-07-07` | |' + )."\n본문 설명 은 유지되어야 한다\n"; + + $out = $this->backfill($content); + + $this->assertStringContainsString('본문 설명 은 유지', $out['result']); + $this->assertSame(1, $out['filled']); + } +} diff --git a/tests/Unit/Console/ApiDocgenExtensionDiscoveryTest.php b/tests/Unit/Console/ApiDocgenExtensionDiscoveryTest.php new file mode 100644 index 00000000..4d5a278a --- /dev/null +++ b/tests/Unit/Console/ApiDocgenExtensionDiscoveryTest.php @@ -0,0 +1,170 @@ + $args 인자 + * @return mixed 반환값 + */ + private function invoke(string $method, array $args): mixed + { + $command = app(ApiDocgenCommand::class); + $ref = new ReflectionMethod($command, $method); + $ref->setAccessible(true); + + return $ref->invokeArgs($command, $args); + } + + #[Test] + public function 확장_컨트롤러_fqc_n에서_베이스_네임스페이스를_추출한다(): void + { + $base = $this->invoke('extensionBaseNamespace', [ + 'Modules\\Sirsoft\\Page\\Http\\Controllers\\Admin\\PageController', + ]); + + $this->assertSame('Modules\\Sirsoft\\Page', $base); + } + + #[Test] + public function http_controllers_없이_controllers_규약을_쓰는_확장도_베이스를_추출한다(): void + { + // pay_kginicis 는 `\Http\Controllers\` 대신 `\Controllers\` 배치를 쓴다. + // 이 경우도 시더 발견을 위해 베이스 네임스페이스를 추출해야 한다. + $base = $this->invoke('extensionBaseNamespace', [ + 'Plugins\\Sirsoft\\PayKginicis\\Controllers\\PaymentSignatureController', + ]); + + $this->assertSame('Plugins\\Sirsoft\\PayKginicis', $base); + } + + #[Test] + public function http_controllers_규약이_controllers_보다_우선한다(): void + { + // 두 세그먼트가 공존할 수 있는 경우 `\Http\Controllers\` 를 우선 판정한다. + $base = $this->invoke('extensionBaseNamespace', [ + 'Modules\\Sirsoft\\Board\\Http\\Controllers\\Admin\\BoardController', + ]); + + $this->assertSame('Modules\\Sirsoft\\Board', $base); + } + + #[Test] + public function 클로저_또는_컨트롤러_규약_밖이면_null을_반환한다(): void + { + $this->assertNull($this->invoke('extensionBaseNamespace', [null])); + $this->assertNull($this->invoke('extensionBaseNamespace', ['SomeRandomClass'])); + } + + #[Test] + public function 파라미터명이_도메인_단수형과_일치하면_매칭이다(): void + { + // pages/{page} → 도메인 단수 리소스 폴백 대상 + $this->assertTrue($this->invoke('paramMatchesDomain', ['page', 'pages'])); + $this->assertTrue($this->invoke('paramMatchesDomain', ['pages', 'pages'])); + } + + #[Test] + public function 보조_문자열_파라미터는_도메인과_매칭되지_않는다(): void + { + // {slug}/{hash}/{versionId} 는 route key 가 달라 폴백 금지 (잘못된 404 회피) + $this->assertFalse($this->invoke('paramMatchesDomain', ['slug', 'pages'])); + $this->assertFalse($this->invoke('paramMatchesDomain', ['hash', 'pages'])); + $this->assertFalse($this->invoke('paramMatchesDomain', ['versionId', 'pages'])); + } + + #[Test] + public function 도메인_샘플의_path_params_맵으로_문자열_파라미터를_치환한다(): void + { + // board 처럼 route-model binding 없는 slug/id 문자열 param 을 + // 도메인 대표 샘플의 path_params 맵(param 명 => 실제 값)으로 정확 일치 치환. + $route = [ + 'uri' => 'api/modules/sirsoft-board/boards/{slug}/posts/{id}', + 'method' => 'GET', + 'domain_group' => 'boards', + 'path_params' => ['slug', 'id'], + 'path_bindings' => [], + ]; + $sampleMap = [ + 'boards' => [ + 'model' => User::class, + 'key' => 'id', + 'value' => '1', + 'path_params' => ['slug' => 'qna', 'id' => '123'], + ], + ]; + + $uri = $this->invoke('resolvePathParams', [$route, $sampleMap]); + + // slug/id 치환 후 중괄호가 모두 사라진 GET 은 목록 대표 샘플 확보용 per_page 가 부착됨 + $this->assertSame('api/modules/sirsoft-board/boards/qna/posts/123?per_page=25', $uri); + } + + #[Test] + public function path_params에_없는_파라미터는_치환하지_않고_원본을_유지한다(): void + { + // path_params 맵에 없는 param(hash) 은 미치환 → URI 에 남아 프로브가 실측 제외. + $route = [ + 'uri' => 'api/modules/sirsoft-board/boards/{slug}/attachment/{hash}', + 'method' => 'GET', + 'domain_group' => 'boards', + 'path_params' => ['slug', 'hash'], + 'path_bindings' => [], + ]; + $sampleMap = [ + 'boards' => [ + 'model' => User::class, + 'key' => 'id', + 'value' => '1', + 'path_params' => ['slug' => 'qna'], + ], + ]; + + $uri = $this->invoke('resolvePathParams', [$route, $sampleMap]); + + // slug 는 치환되고 hash 는 미치환 → 중괄호가 남아 실측 대상에서 제외됨 + $this->assertStringContainsString('boards/qna/attachment/{hash}', $uri); + } + + #[Test] + public function path_params가_없는_기존_확장은_route_key_치환만_적용한다(): void + { + // page/gdpr 등 path_params 미제공 확장은 기존 동작 유지(회귀 방지): + // 문자열 param 은 미치환 → 원본 중괄호 유지. + $route = [ + 'uri' => 'api/modules/sirsoft-page/pages/{slug}', + 'method' => 'GET', + 'domain_group' => 'pages', + 'path_params' => ['slug'], + 'path_bindings' => [], + ]; + $sampleMap = [ + 'pages' => [ + 'model' => User::class, + 'key' => 'slug', + 'value' => 'sample', + ], + ]; + + $uri = $this->invoke('resolvePathParams', [$route, $sampleMap]); + + // path_params 미제공 + paramMatchesDomain('slug','pages')=false → 미치환 + $this->assertStringContainsString('pages/{slug}', $uri); + } +} diff --git a/tests/Unit/Support/ApiDoc/ApiDocPipelineTest.php b/tests/Unit/Support/ApiDoc/ApiDocPipelineTest.php new file mode 100644 index 00000000..cb4d1c00 --- /dev/null +++ b/tests/Unit/Support/ApiDoc/ApiDocPipelineTest.php @@ -0,0 +1,312 @@ + true, + 'message' => '조회 성공', + 'data' => [ + 'data' => [ + ['id' => 1, 'name' => '홍길동', 'is_active' => true, 'deleted' => null], + ], + 'pagination' => ['current_page' => 1, 'total' => 10], + ], + ]; + + $schema = $inferrer->infer($body); + + $this->assertSame('collection', $schema['shape']); + $this->assertTrue($schema['pagination']); + $this->assertSame(['success', 'message', 'data'], $schema['envelope']); + + $fields = collect($schema['fields'])->keyBy('name'); + $this->assertSame('integer', $fields['id']['type']); + $this->assertSame('string', $fields['name']['type']); + $this->assertSame('boolean', $fields['is_active']['type']); + $this->assertSame('null', $fields['deleted']['type']); + $this->assertSame('홍길동', $fields['name']['sample']); + } + + #[Test] + public function 단건_응답에서_객체_필드를_추론한다(): void + { + $inferrer = new ResponseSchemaInferrer; + + $body = [ + 'success' => true, + 'data' => ['total' => 155, 'ratio' => 0.5, 'labels' => ['ko' => 91]], + ]; + + $schema = $inferrer->infer($body); + + $this->assertSame('object', $schema['shape']); + $this->assertFalse($schema['pagination']); + + $fields = collect($schema['fields'])->keyBy('name'); + $this->assertSame('integer', $fields['total']['type']); + $this->assertSame('number', $fields['ratio']['type']); + $this->assertSame('object', $fields['labels']['type']); + } + + #[Test] + public function 표_셀에서_파이프_문자를_이스케이프한다(): void + { + $inferrer = new ResponseSchemaInferrer; + + $body = ['success' => true, 'data' => ['note' => 'a|b|c']]; + $schema = $inferrer->infer($body); + + $fields = collect($schema['fields'])->keyBy('name'); + $this->assertStringNotContainsString('|b', str_replace('\\|', '', $fields['note']['sample'])); + $this->assertStringContainsString('\\|', $fields['note']['sample']); + } + + #[Test] + public function 스캐폴더가_엔드포인트_섹션을_표준_포맷으로_생성한다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', + 'uri' => '/api/admin/users', + 'name' => 'api.admin.users.index', + 'controller' => 'App\\Http\\Controllers\\Api\\Admin\\UserController', + 'controller_method' => 'index', + 'permission' => 'core.users.read', + 'middleware' => ['auth:sanctum', 'App\\Http\\Middleware\\AdminMiddleware'], + 'path_params' => [], + ]; + + $request = ['request_class' => 'X', 'params' => [ + ['name' => 'page', 'type' => 'integer', 'required' => false, 'allowed' => 'min 1'], + ], 'hook_filters' => ['core.user.list_validation_rules']]; + + $schema = [ + 'envelope' => ['success', 'data'], + 'shape' => 'collection', + 'fields' => [['name' => 'id', 'type' => 'integer', 'sample' => '1']], + 'pagination' => true, + ]; + + $section = $scaffolder->endpointSection($route, $request, $schema, ['status' => 200, 'skipped_reason' => null]); + + $this->assertStringContainsString('### GET /api/admin/users', $section); + $this->assertStringContainsString('@generated:start:api.admin.users.index', $section); + $this->assertStringContainsString('`auth:sanctum` + `admin` + `permission:core.users.read`', $section); + $this->assertStringContainsString('| page | query | integer | 아니오 | min 1 |', $section); + $this->assertStringContainsString('core.user.list_validation_rules', $section); + $this->assertStringContainsString('| id | integer | `1` |', $section); + // 에러 응답 표: auth:sanctum→401, admin+permission→403, FormRequest(params/hook)→422 + $this->assertStringContainsString('**에러 응답**', $section); + $this->assertStringContainsString('| 401 | Unauthenticated |', $section); + $this->assertStringContainsString('| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 |', $section); + $this->assertStringContainsString('| 422 | Unprocessable Entity |', $section); + $this->assertStringContainsString('@generated:end', $section); + // 에러 표는 @generated 블록 내부(재생성 대상)여야 한다 + $genStart = strpos($section, '@generated:start'); + $genEnd = strpos($section, '@generated:end'); + $errorPos = strpos($section, '**에러 응답**'); + $this->assertGreaterThan($genStart, $errorPos); + $this->assertLessThan($genEnd, $errorPos); + } + + #[Test] + public function 에러_섹션이_라우트_메타에서_대표_상태코드를_추론한다(): void + { + $scaffolder = new ApiDocScaffolder; + + // path param 존재 + admin + permission + FormRequest → 401/403/422/404 전부 + $route = [ + 'method' => 'PUT', + 'uri' => '/api/admin/users/{user}', + 'name' => 'api.admin.users.update', + 'controller' => 'C', 'controller_method' => 'update', + 'permission' => 'core.users.update', + 'middleware' => ['auth:sanctum', 'App\\Http\\Middleware\\AdminMiddleware'], + 'path_params' => ['user'], + ]; + $request = ['request_class' => 'X', 'params' => [ + ['name' => 'name', 'type' => 'string', 'required' => true, 'allowed' => ''], + ], 'hook_filters' => []]; + + $section = $scaffolder->endpointSection($route, $request, null, ['status' => null, 'skipped_reason' => 'write-method']); + + $this->assertStringContainsString('| 401 | Unauthenticated |', $section); + $this->assertStringContainsString('| 403 | Forbidden | 요구 권한(`core.users.update`)이 없는 경우 |', $section); + $this->assertStringContainsString('| 422 | Unprocessable Entity |', $section); + $this->assertStringContainsString('| 404 | Not Found |', $section); + } + + #[Test] + public function optional_sanctum_공개조회는_401을_유발하지_않는다(): void + { + // optional.sanctum(선택 인증)은 미인증도 허용 → 401 없음. + // path param 만 있으므로 404 만 노출. + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', + 'uri' => '/api/modules/sirsoft-board/boards/{slug}', + 'name' => 'api.modules.sirsoft-board.boards.show', + 'controller' => 'C', 'controller_method' => 'show', + 'permission' => null, + 'middleware' => ['api', 'optional.sanctum'], + 'path_params' => ['slug'], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['data'], 'shape' => 'object', 'fields' => [['name' => 'id', 'type' => 'integer', 'sample' => '1']], 'pagination' => false]; + + $section = $scaffolder->endpointSection($route, $request, $schema, ['status' => 200, 'skipped_reason' => null]); + + $this->assertStringNotContainsString('| 401 |', $section); + $this->assertStringNotContainsString('| 403 |', $section); + $this->assertStringNotContainsString('| 422 |', $section); + $this->assertStringContainsString('| 404 | Not Found |', $section); + } + + #[Test] + public function 완전_공개_조회는_대표_에러_없음으로_표기한다(): void + { + // 인증·권한·FormRequest·path param 전무 → 대표 에러 없음. + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', 'uri' => '/api/locales', 'name' => 'api.locales.index', + 'controller' => 'C', 'controller_method' => 'index', 'permission' => null, + 'middleware' => ['api'], 'path_params' => [], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['data'], 'shape' => 'collection', 'fields' => [['name' => 'code', 'type' => 'string', 'sample' => 'ko']], 'pagination' => false]; + + $section = $scaffolder->endpointSection($route, $request, $schema, ['status' => 200, 'skipped_reason' => null]); + + $this->assertStringContainsString('대표 에러 없음', $section); + } + + #[Test] + public function optional_sanctum_라우트는_선택적_인증으로_표기한다(): void + { + // optional.sanctum(회원/비회원 모두 접근)을 auth:sanctum(인증 필수)로 + // 오표기하면 공개 API 계약이 왜곡된다(게시판 공개 조회 등). 별도 표기 강제. + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', + 'uri' => '/api/modules/sirsoft-board/boards/{slug}/posts', + 'name' => 'api.modules.sirsoft-board.boards.posts.index', + 'controller' => 'C', 'controller_method' => 'index', + 'permission' => 'user,sirsoft-board.{slug}.posts.read', + 'middleware' => ['api', 'optional.sanctum', 'throttle:600,1'], + 'path_params' => ['slug'], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['data'], 'shape' => 'collection', 'fields' => [], 'pagination' => true]; + + $section = $scaffolder->endpointSection($route, $request, $schema, ['status' => 200, 'skipped_reason' => null]); + + // optional.sanctum 은 선택적 인증으로 표기되고 auth:sanctum 으로 오표기되지 않는다 + $this->assertStringContainsString('optional.sanctum', $section); + $this->assertStringNotContainsString('`auth:sanctum`', $section); + } + + #[Test] + public function 컬럼_주석이_있으면_응답_필드_설명으로_채운다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', 'uri' => '/api/admin/users', 'name' => 'api.admin.users.index', + 'controller' => 'C', 'controller_method' => 'index', 'permission' => null, + 'middleware' => [], 'path_params' => [], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = [ + 'envelope' => ['data'], 'shape' => 'object', + 'fields' => [ + ['name' => 'nickname', 'type' => 'string', 'sample' => 'hong'], + ['name' => 'unknown_field', 'type' => 'string', 'sample' => 'x'], + ], + 'pagination' => false, + ]; + $commentMap = ['nickname' => '닉네임']; + + $section = $scaffolder->endpointSection($route, $request, $schema, ['status' => 200, 'skipped_reason' => null], $commentMap); + + // 주석 있는 필드는 설명이 채워지고, 없는 필드는 TODO 유지 + $this->assertStringContainsString('| nickname | string | `hong` | 닉네임 |', $section); + $this->assertStringContainsString('| unknown_field | string | `x` | |', $section); + } + + #[Test] + public function 쓰기_메서드는_응답_필드를_실측_제외로_표기한다(): 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' => [], + ]; + + $section = $scaffolder->endpointSection( + $route, + ['request_class' => null, 'params' => [], 'hook_filters' => []], + null, + ['status' => null, 'skipped_reason' => 'write-method'] + ); + + $this->assertStringContainsString('실측 제외: write-method', $section); + } + + #[Test] + public function 재생성_시_사람이_작성한_설명을_보존한다(): void + { + $scaffolder = new ApiDocScaffolder; + + $route = [ + 'method' => 'GET', 'uri' => '/api/x', 'name' => 'api.x.index', + 'controller' => 'C', 'controller_method' => 'index', 'permission' => null, + 'middleware' => [], 'path_params' => [], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['data'], 'shape' => 'object', 'fields' => [['name' => 'a', 'type' => 'integer', 'sample' => '1']], 'pagination' => false]; + + $section = $scaffolder->endpointSection($route, $request, $schema, ['status' => 200, 'skipped_reason' => null]); + $header = "# X\n"; + + // 최초 생성 + $first = $scaffolder->mergeDocument(null, $header, [$section], ['api.x.index']); + $this->assertStringContainsString('TODO: 이 엔드포인트의 용도', $first); + + // 사람이 설명을 채운 상태 + $withProse = str_replace( + '**설명** ', + '**설명** 실제 사람이 작성한 설명입니다.', + $first + ); + + // 재생성: 새 섹션으로 병합해도 사람 서술 보존 + $regenerated = $scaffolder->mergeDocument($withProse, $header, [$section], ['api.x.index']); + + $this->assertStringContainsString('실제 사람이 작성한 설명입니다.', $regenerated); + $this->assertStringNotContainsString('TODO: 이 엔드포인트의 용도', $regenerated); + } +} diff --git a/tests/Unit/Support/ApiDoc/ApiRouteInventoryFallbackTest.php b/tests/Unit/Support/ApiDoc/ApiRouteInventoryFallbackTest.php new file mode 100644 index 00000000..8e17b3ed --- /dev/null +++ b/tests/Unit/Support/ApiDoc/ApiRouteInventoryFallbackTest.php @@ -0,0 +1,75 @@ +collect('module:gnuboard7-hello_module'); + + // api.php 의 공개 memos GET 2건(index/show)이 폴백으로 수집된다. + $this->assertNotEmpty($routes, '미설치 모듈의 번들 라우트가 폴백으로 수집되어야 한다'); + + $names = array_column($routes, 'name'); + $this->assertContains('api.modules.gnuboard7-hello_module.memos.index', $names); + $this->assertContains('api.modules.gnuboard7-hello_module.memos.show', $names); + } + + #[Test] + public function 폴백_수집_라우트는_프로바이더와_동일한_ur_l_규약을_따른다(): void + { + $routes = app(ApiRouteInventory::class)->collect('module:gnuboard7-hello_module'); + + $index = collect($routes)->firstWhere('name', 'api.modules.gnuboard7-hello_module.memos.index'); + + $this->assertNotNull($index); + $this->assertSame('GET', $index['method']); + $this->assertSame('/api/modules/gnuboard7-hello_module/memos', $index['uri']); + $this->assertSame('module', $index['owner']['type']); + $this->assertSame('gnuboard7-hello_module', $index['owner']['id']); + $this->assertSame('memos', $index['domain_group']); + $this->assertSame( + 'Modules\\Gnuboard7\\HelloModule\\Http\\Controllers\\Api\\MemoController', + $index['controller'] + ); + } + + #[Test] + public function web_admin_라우트는_ap_i_문서_대상에서_제외된다(): void + { + // hello_module 의 admin CRUD 는 web.php(/modules/{id} prefix)라 api/ 로 시작하지 않아 + // API 문서 대상이 아니다. 폴백은 src/routes/api.php 만 로드한다. + $routes = app(ApiRouteInventory::class)->collect('module:gnuboard7-hello_module'); + + foreach ($routes as $route) { + $this->assertStringStartsWith('/api/', $route['uri']); + } + + $names = array_column($routes, 'name'); + $this->assertNotContains('api.modules.gnuboard7-hello_module.admin.memos.store', $names); + } + + #[Test] + public function 라우트_파일이_없는_범위는_빈_배열을_반환한다(): void + { + $routes = app(ApiRouteInventory::class)->collect('module:nonexistent-module-xyz'); + + $this->assertSame([], $routes); + } +} diff --git a/tests/Unit/Support/ApiDoc/ParameterDescriberTest.php b/tests/Unit/Support/ApiDoc/ParameterDescriberTest.php new file mode 100644 index 00000000..7733a425 --- /dev/null +++ b/tests/Unit/Support/ApiDoc/ParameterDescriberTest.php @@ -0,0 +1,239 @@ +assertStringContainsString('페이지 번호', $describer->describe('page', 'query', 'integer')); + $this->assertStringContainsString('페이지당', $describer->describe('per_page', 'query', 'integer')); + $this->assertStringContainsString('정렬 기준 필드', $describer->describe('sort_by', 'query', 'string')); + $this->assertStringContainsString('검색어', $describer->describe('search', 'query', 'string')); + $this->assertStringContainsString('필터', $describer->describe('filters', 'query', 'array')); + } + + #[Test] + public function 기간_토글_파라미터를_설명한다(): void + { + $describer = new ParameterDescriber; + + $this->assertStringContainsString('시작일', $describer->describe('start_date', 'query', 'date')); + $this->assertStringContainsString('종료일', $describer->describe('end_date', 'query', 'date')); + $this->assertStringContainsString('활성', $describer->describe('is_active', 'query', 'boolean')); + $this->assertStringContainsString('강제', $describer->describe('force', 'body', 'boolean')); + } + + #[Test] + public function sort_order_는_타입으로_방향과_순서값을_구분한다(): void + { + $describer = new ParameterDescriber; + + // 문자열 asc/desc → 정렬 방향 + $this->assertStringContainsString('정렬 방향', $describer->describe('sort_order', 'query', 'string')); + $this->assertStringContainsString('정렬 방향', $describer->describe('order', 'query', 'string')); + // 정수 → 표시 순서 값 + $this->assertStringContainsString('표시 정렬 순서', $describer->describe('sort_order', 'body', 'integer')); + $this->assertStringContainsString('표시 정렬 순서', $describer->describe('order', 'body', 'integer')); + } + + #[Test] + public function path_식별자_파라미터를_위치_기반으로_설명한다(): void + { + $describer = new ParameterDescriber; + + $this->assertSame('대상 리소스의 식별자', $describer->describe('id', 'path', 'string')); + $this->assertStringContainsString('slug', $describer->describe('slug', 'path', 'string')); + $this->assertSame('대상 리소스의 식별자', $describer->describe('identifier', 'path', 'string')); + } + + #[Test] + public function path_카멜케이스_식별자를_패턴으로_설명한다(): void + { + $describer = new ParameterDescriber; + + // *Id → "대상 {base}의 식별자" + $this->assertSame('대상 post의 식별자', $describer->describe('postId', 'path', 'string')); + $this->assertSame('대상 product의 식별자', $describer->describe('productId', 'path', 'string')); + // *Name → "대상 {base}의 이름 (식별자)" + $this->assertStringContainsString('template', $describer->describe('templateName', 'path', 'string')); + $this->assertStringContainsString('이름', $describer->describe('pluginName', 'path', 'string')); + } + + #[Test] + public function path_bare_리소스명은_route_model_binding_식별자로_설명한다(): void + { + $describer = new ParameterDescriber; + + // Laravel route-model binding 세그먼트(`/{definition}`, `/{menu}`, `/{role}`)는 + // 대상 모델의 단수형을 그대로 쓰므로 대상 리소스의 식별자로 서술한다. + $this->assertSame('대상 definition의 식별자', $describer->describe('definition', 'path', 'string')); + $this->assertSame('대상 menu의 식별자', $describer->describe('menu', 'path', 'string')); + $this->assertSame('대상 role의 식별자', $describer->describe('role', 'path', 'string')); + $this->assertSame('대상 schedule의 식별자', $describer->describe('schedule', 'path', 'string')); + $this->assertSame('대상 challenge의 식별자', $describer->describe('challenge', 'path', 'string')); + // camelCase route-model binding (activityLog, notificationLog) + $this->assertSame('대상 activity log의 식별자', $describer->describe('activityLog', 'path', 'string')); + // *Identifier 접미 + $this->assertSame('대상 template의 식별자', $describer->describe('templateIdentifier', 'path', 'string')); + // key/version 은 리소스가 아니라 설정 키/버전 값 + $this->assertStringContainsString('키', $describer->describe('key', 'path', 'string')); + $this->assertStringContainsString('버전', $describer->describe('version', 'path', 'string')); + // bare path 리소스명은 자동 서술되므로 query/body 위치에서만 도메인 특이 판정이 유지된다 + $this->assertNull($describer->describe('definition', 'body', 'string')); + } + + #[Test] + public function 연관_식별자_snake_패턴을_설명한다(): void + { + $describer = new ParameterDescriber; + + // query/body 의 *_id 는 연관 리소스 식별자 참조 + $this->assertSame('user 식별자', $describer->describe('user_id', 'body', 'integer')); + $this->assertSame('shipping policy 식별자', $describer->describe('shipping_policy_id', 'query', 'integer')); + // *_ids 는 배열 + $this->assertStringContainsString('배열', $describer->describe('product_ids', 'body', 'array')); + } + + #[Test] + public function 불리언_날짜_접미_패턴을_설명한다(): void + { + $describer = new ParameterDescriber; + + $this->assertSame('featured 여부', $describer->describe('is_featured', 'query', 'boolean')); + $this->assertSame('paid 날짜', $describer->describe('paid_date', 'body', 'date')); + } + + #[Test] + public function 프로필_콘텐츠_공통_필드를_설명한다(): void + { + $describer = new ParameterDescriber; + + $this->assertStringContainsString('이름', $describer->describe('name', 'body', 'string')); + $this->assertStringContainsString('닉네임', $describer->describe('nickname', 'body', 'string')); + $this->assertStringContainsString('설명', $describer->describe('description', 'body', 'string')); + $this->assertStringContainsString('본문', $describer->describe('content', 'body', 'array')); + $this->assertStringContainsString('제목', $describer->describe('subject', 'body', 'array')); + $this->assertStringContainsString('전화', $describer->describe('phone', 'body', 'string')); + $this->assertStringContainsString('휴대전화', $describer->describe('mobile', 'body', 'string')); + $this->assertStringContainsString('자기소개', $describer->describe('bio', 'body', 'string')); + $this->assertStringContainsString('경로', $describer->describe('path', 'query', 'string')); + $this->assertStringContainsString('사용자명', $describer->describe('username', 'body', 'string')); + // collection: 첨부 컬렉션 그룹명 (근거: UploadAttachmentRequest collection ?? 'default') + $this->assertStringContainsString('첨부 컬렉션', $describer->describe('collection', 'body', 'string')); + } + + #[Test] + public function 확장_버전_공통_파라미터를_설명한다(): void + { + $describer = new ParameterDescriber; + + $this->assertStringContainsString('확장 유형', $describer->describe('extension_type', 'body', 'string')); + $this->assertStringContainsString('확장 식별자', $describer->describe('extension_identifier', 'body', 'string')); + $this->assertStringContainsString('시작 버전', $describer->describe('from_version', 'query', 'string')); + $this->assertStringContainsString('대상 버전', $describer->describe('to_version', 'query', 'string')); + $this->assertStringContainsString('GitHub', $describer->describe('github_url', 'body', 'string')); + $this->assertStringContainsString('체크섬', $describer->describe('checksum', 'body', 'string')); + $this->assertStringContainsString('자동 활성화', $describer->describe('auto_activate', 'body', 'boolean')); + // query/body identifier 는 확장/리소스 식별자 + $this->assertStringContainsString('식별자', $describer->describe('identifier', 'query', 'string')); + } + + #[Test] + public function 확장_이름_snake_패턴을_설명한다(): void + { + $describer = new ParameterDescriber; + + $this->assertStringContainsString('이름', $describer->describe('template_name', 'body', 'string')); + $this->assertStringContainsString('이름', $describer->describe('plugin_name', 'body', 'string')); + $this->assertStringContainsString('이름', $describer->describe('module_name', 'body', 'string')); + $this->assertStringContainsString('이름', $describer->describe('layout_name', 'query', 'string')); + } + + #[Test] + public function 스케줄_작업_공통_파라미터를_설명한다(): void + { + $describer = new ParameterDescriber; + + $this->assertStringContainsString('아티즌 커맨드', $describer->describe('command', 'body', 'string')); + $this->assertStringContainsString('주기', $describer->describe('frequency', 'body', 'string')); + $this->assertStringContainsString('타임아웃', $describer->describe('timeout', 'body', 'integer')); + $this->assertStringContainsString('점검 모드', $describer->describe('run_in_maintenance', 'body', 'boolean')); + $this->assertStringContainsString('중복 실행', $describer->describe('without_overlapping', 'body', 'boolean')); + $this->assertStringContainsString('잠금 버전', $describer->describe('expected_lock_version', 'body', 'integer')); + } + + #[Test] + public function 메일_드라이버_설정_파라미터를_설명한다(): void + { + $describer = new ParameterDescriber; + + $this->assertStringContainsString('메일 발송 드라이버', $describer->describe('mailer', 'body', 'string')); + $this->assertStringContainsString('발신자 주소', $describer->describe('from_address', 'body', 'email')); + $this->assertStringContainsString('호스트', $describer->describe('host', 'body', 'string')); + $this->assertStringContainsString('포트', $describer->describe('port', 'body', 'integer')); + $this->assertStringContainsString('스토리지 드라이버', $describer->describe('storage_driver', 'body', 'string')); + $this->assertStringContainsString('S3 버킷', $describer->describe('s3_bucket', 'body', 'string')); + $this->assertStringContainsString('Redis 호스트', $describer->describe('redis_host', 'body', 'string')); + $this->assertStringContainsString('WebSocket', $describer->describe('websocket_scheme', 'body', 'string')); + } + + #[Test] + public function 도메인_특이_파라미터는_null_로_남긴다(): void + { + $describer = new ParameterDescriber; + + // 사전/패턴에 없는 도메인 특이 파라미터는 사람 서술(TODO)로 폴백 + $this->assertNull($describer->describe('refund_priority', 'body', 'string')); + $this->assertNull($describer->describe('temp_key', 'body', 'string')); + // 도메인마다 의미가 갈리는 파라미터는 계속 TODO 유지 + $this->assertNull($describer->describe('channels', 'body', 'array')); + $this->assertNull($describer->describe('purpose', 'body', 'string')); + $this->assertNull($describer->describe('scope_type', 'body', 'string')); + $this->assertNull($describer->describe('conditions', 'body', 'array')); + $this->assertNull($describer->describe('marketing_consent', 'body', 'boolean')); + // source_type 은 Enum 기반으로 도메인마다 값 집합이 달라 TODO 유지 + $this->assertNull($describer->describe('source_type', 'body', 'string')); + // body(생성/수정)의 status/type/category 는 여전히 TODO + $this->assertNull($describer->describe('status', 'body', 'string')); + $this->assertNull($describer->describe('type', 'body', 'string')); + } + + #[Test] + public function 공통_콘텐츠_주소_seo_파라미터를_설명한다(): void + { + $describer = new ParameterDescriber; + + // title: board 게시글/memo/inquiry/page 등 전 도메인에서 "제목"으로 고정 + // (근거: sirsoft-board/page/hello_module/ecommerce Store/Update Request) + $this->assertStringContainsString('제목', $describer->describe('title', 'body', 'string')); + // slug: URL 친화 식별자 (위치 무관 동일 의미) + $this->assertStringContainsString('slug', $describer->describe('slug', 'body', 'string')); + $this->assertStringContainsString('라벨', $describer->describe('label', 'body', 'string')); + + // 주소 확장 (국제 주소 표준 — 도메인 무관) + $this->assertStringContainsString('주소', $describer->describe('address_line_1', 'body', 'string')); + $this->assertStringContainsString('주소', $describer->describe('address_line_2', 'body', 'string')); + $this->assertStringContainsString('도시', $describer->describe('intl_city', 'body', 'string')); + $this->assertStringContainsString('지역', $describer->describe('region', 'query', 'string')); + + // SEO 메타 (검색엔진 노출용 — 도메인 무관) + $this->assertStringContainsString('SEO', $describer->describe('meta_title', 'body', 'string')); + $this->assertStringContainsString('SEO', $describer->describe('meta_description', 'body', 'string')); + $this->assertStringContainsString('대체 텍스트', $describer->describe('alt_text', 'body', 'array')); + } +} diff --git a/tests/Unit/Support/ApiDoc/ResourceFieldDescriberTest.php b/tests/Unit/Support/ApiDoc/ResourceFieldDescriberTest.php new file mode 100644 index 00000000..e28bd087 --- /dev/null +++ b/tests/Unit/Support/ApiDoc/ResourceFieldDescriberTest.php @@ -0,0 +1,259 @@ +assertStringContainsString('소유자', $describer->describe('is_owner', 'boolean')); + $this->assertStringContainsString('작업 불리언 맵', $describer->describe('abilities', 'object')); + $this->assertStringContainsString('Enum label()', $describer->describe('status_label', 'string')); + $this->assertStringContainsString('관리자', $describer->describe('is_admin', 'boolean')); + } + + #[Test] + public function 타임스탬프_접미_패턴을_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + $this->assertSame('생성 일시', $describer->describe('created_at', 'string')); + $this->assertSame('최종 수정 일시', $describer->describe('updated_at', 'string')); + // 사전에 없는 *_at 는 패턴으로 유추 + $this->assertSame('email verified 일시', $describer->describe('email_verified_at', 'string')); + } + + #[Test] + public function label_variant_접미_패턴을_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + $desc = $describer->describe('priority_label', 'string'); + $this->assertStringContainsString('priority', $desc); + $this->assertStringContainsString('라벨', $desc); + + $variant = $describer->describe('priority_variant', 'string'); + $this->assertStringContainsString('변형', $variant); + } + + #[Test] + public function can_접두는_능력_불리언으로_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + $desc = $describer->describe('can_update', 'boolean'); + $this->assertStringContainsString('update', $desc); + $this->assertStringContainsString('수행 가능', $desc); + } + + #[Test] + public function is_has_접두_불리언을_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + $this->assertStringContainsString('여부', $describer->describe('is_default', 'boolean')); + $this->assertStringContainsString('여부', $describer->describe('has_children', 'boolean')); + // boolean 이 아니면 패턴 미적용 (오설명 방지) + $this->assertNull($describer->describe('is_default', 'string')); + } + + #[Test] + public function count_접미_집계를_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + $this->assertStringContainsString('개수', $describer->describe('user_count', 'integer')); + // integer/number 가 아니면 미적용 + $this->assertNull($describer->describe('user_count', 'string')); + } + + #[Test] + public function 알_수_없는_필드는_null_을_반환한다(): void + { + $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 도메인 특이 필드는 제외 + $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')); + $this->assertNull($describer->describe('code', 'string')); + } + + #[Test] + public function 관계_연관_객체_필드를_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + $this->assertStringContainsString('생성자', $describer->describe('creator', 'object')); + $this->assertStringContainsString('하위', $describer->describe('children', 'array')); + $this->assertStringContainsString('상위', $describer->describe('parent', 'object')); + $this->assertStringContainsString('권한', $describer->describe('permissions', 'array')); + $this->assertStringContainsString('수신자', $describer->describe('recipient', 'object')); + $this->assertStringContainsString('발신자', $describer->describe('sender', 'object')); + $this->assertStringContainsString('작성자', $describer->describe('author', 'object')); + $this->assertStringContainsString('행위', $describer->describe('actor_name', 'string')); + } + + #[Test] + public function 공통_콘텐츠_표시_필드를_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + // 도메인 무관하게 역할이 고정된 공통 콘텐츠/표시 필드 (요청 파라미터 대응물) + $this->assertStringContainsString('이름', $describer->describe('name', 'object')); + $this->assertStringContainsString('제목', $describer->describe('title', 'string')); + $this->assertStringContainsString('본문', $describer->describe('content', 'string')); + $this->assertStringContainsString('설명', $describer->describe('description', 'object')); + $this->assertStringContainsString('slug', $describer->describe('slug', 'string')); + $this->assertStringContainsString('라벨', $describer->describe('label', 'string')); + $this->assertStringContainsString('아이콘', $describer->describe('icon', 'string')); + $this->assertStringContainsString('썸네일', $describer->describe('thumbnail', 'string')); + $this->assertStringContainsString('IP', $describer->describe('ip_address', 'string')); + } + + #[Test] + public function sort_order_는_타입으로_분기한다(): void + { + $describer = new ResourceFieldDescriber; + + // 정수: 표시 정렬 순서 값 + $this->assertStringContainsString('정렬 순서', $describer->describe('sort_order', 'integer')); + // 문자열: 정렬 방향일 수 있어 도메인 특이 → null (오설명 방지) + $this->assertNull($describer->describe('sort_order', 'string')); + } + + #[Test] + public function 시스템_집계_필드를_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + $this->assertStringContainsString('전체', $describer->describe('total', 'integer')); + $this->assertStringContainsString('사용자', $describer->describe('total_users', 'object')); + $this->assertStringContainsString('상대 시각', $describer->describe('time', 'string')); + $this->assertStringContainsString('서버', $describer->describe('server_time', 'string')); + $this->assertStringContainsString('순번', $describer->describe('number', 'integer')); + } + + #[Test] + public function 확장_버전_로케일_필드를_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + $this->assertStringContainsString('타입', $describer->describe('extension_type', 'string')); + $this->assertStringContainsString('식별자', $describer->describe('extension_identifier', 'string')); + $this->assertStringContainsString('이름', $describer->describe('extension_name', 'string')); + $this->assertStringContainsString('GitHub', $describer->describe('github_url', 'string')); + $this->assertStringContainsString('변경 이력', $describer->describe('changelog', 'string')); + $this->assertStringContainsString('코어', $describer->describe('current_core_version', 'string')); + $this->assertStringContainsString('로케일', $describer->describe('locales', 'array')); + $this->assertStringContainsString('표시명', $describer->describe('locale_names', 'object')); + $this->assertStringContainsString('차단', $describer->describe('install_blocked_reason', 'string')); + } + + #[Test] + public function raw_접미_패턴은_원본_값으로_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + $nameRaw = $describer->describe('name_raw', 'string'); + $this->assertStringContainsString('name', $nameRaw); + $this->assertStringContainsString('원본', $nameRaw); + + $descRaw = $describer->describe('description_raw', 'string'); + $this->assertStringContainsString('description', $descRaw); + $this->assertStringContainsString('원본', $descRaw); + } + + #[Test] + public function formatted_접미는_표시용_포맷_문자열로_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + // 근거: size_formatted accessor, formatCurrencyPrice/formatFileSize/ + // formatCreatedAtFormat — 통화·용량·일시 포맷 파생 + $created = $describer->describe('created_at_formatted', 'string'); + $this->assertStringContainsString('created_at', $created); + $this->assertStringContainsString('포맷', $created); + + $price = $describer->describe('selling_price_formatted', 'string'); + $this->assertStringContainsString('selling_price', $price); + $this->assertStringContainsString('포맷', $price); + } + + #[Test] + public function localized_접두_접미는_로케일_해석_값으로_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + // 근거: getLocalizedName() / getLocalizedOptionName() + $localizedName = $describer->describe('localized_name', 'string'); + $this->assertStringContainsString('name', $localizedName); + $this->assertStringContainsString('로케일', $localizedName); + + $optionLocalized = $describer->describe('option_name_localized', 'string'); + $this->assertStringContainsString('option name', $optionLocalized); + $this->assertStringContainsString('로케일', $optionLocalized); + } + + #[Test] + public function url_접미는_리소스_url로_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + // 근거: thumbnail_url => download_url / getThumbnailUrl() + $thumb = $describer->describe('thumbnail_url', 'string'); + $this->assertStringContainsString('thumbnail', $thumb); + $this->assertStringContainsString('URL', $thumb); + } + + #[Test] + public function id_접미는_참조_식별자로_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + // 응답의 *_id 는 예외 없이 연관 리소스 참조 식별자다 (parent_id/user_id 등) + $parent = $describer->describe('parent_id', 'integer'); + $this->assertStringContainsString('parent', $parent); + $this->assertStringContainsString('식별자', $parent); + + $ids = $describer->describe('permission_ids', 'array'); + $this->assertStringContainsString('permission', $ids); + $this->assertStringContainsString('배열', $ids); + + // 예외: login_id 는 참조 식별자가 아니라 로그인 계정 아이디(문자열) → TODO 유지 + $this->assertNull($describer->describe('user_login_id', 'string')); + } + + #[Test] + public function depth_는_계층_트리_깊이로_설명한다(): void + { + $describer = new ResourceFieldDescriber; + + // 근거: CommentResource/CategoryResource 의 depth (children/parent 트리) + $depth = $describer->describe('depth', 'integer'); + $this->assertStringContainsString('깊이', $depth); + // 정수가 아니면 미적용 (오설명 방지) + $this->assertNull($describer->describe('depth', 'string')); + } +}