Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a34a03eb40 | ||
|
|
b8ceb26cf3 | ||
|
|
feca0c7146 | ||
|
|
1ec9692cac | ||
|
|
b65c782447 | ||
|
|
2832db6ffd | ||
|
|
f432df9d9c | ||
|
|
edb4d13096 | ||
|
|
abe3fe85d4 | ||
|
|
809409f3cf | ||
|
|
12968b76ca | ||
|
|
051ed1e528 | ||
|
|
0f84657a5c | ||
|
|
0ff4b674ac | ||
|
|
e3c00addd8 | ||
|
|
0200f1d043 | ||
|
|
5f99b104c1 | ||
|
|
5f3b904aab | ||
|
|
45bf59472a | ||
|
|
2493bf424a | ||
|
|
c26b49a997 | ||
|
|
13aeb433cc | ||
|
|
168f439be3 | ||
|
|
56a78e1d55 | ||
|
|
70452745c4 | ||
|
|
79eb091351 | ||
|
|
848b408b60 | ||
|
|
49b21e788c | ||
|
|
dbbc343734 | ||
|
|
88116de788 | ||
|
|
c6cab1f668 | ||
|
|
f744b816b6 | ||
|
|
b9355f051c | ||
|
|
ff45a19b4b | ||
|
|
cebe352e46 | ||
|
|
48f877bdcd | ||
|
|
e7e2936ea0 | ||
|
|
8429d8ada5 | ||
|
|
d18aaab1fd | ||
|
|
646efeb91c | ||
|
|
dee5a12ded | ||
|
|
c59819645a | ||
|
|
1cdef4513a | ||
|
|
5a9ad97327 | ||
|
|
b428734a06 | ||
|
|
0e9bb39980 | ||
|
|
2a9f521487 | ||
|
|
76f7300e8f | ||
|
|
022d629144 | ||
|
|
f0d44989e1 | ||
|
|
9863ea9d3a | ||
|
|
479d537dfb | ||
|
|
657f546efc |
+1
-1
@@ -3,7 +3,7 @@ APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=http://localhost
|
||||
APP_VERSION=7.0.2
|
||||
APP_VERSION=7.0.3
|
||||
|
||||
APP_LOCALE=ko
|
||||
APP_FALLBACK_LOCALE=ko
|
||||
|
||||
@@ -3,7 +3,7 @@ APP_ENV=testing
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=http://localhost
|
||||
APP_VERSION=7.0.2
|
||||
APP_VERSION=7.0.3
|
||||
|
||||
APP_LOCALE=ko
|
||||
APP_FALLBACK_LOCALE=ko
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 질문·사용법 문의 (sir.kr Q&A)
|
||||
url: https://sir.kr/questions/create?tag=%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
|
||||
<!-- AUTO-GENERATED-START: docs-quick-reference -->
|
||||
|
||||
### 백엔드 [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) |
|
||||
@@ -116,7 +117,7 @@
|
||||
| [module-i18n.md](docs/extension/module-i18n.md) | 모듈 다국어 시스템 | 백엔드: /lang/{locale}/*.php → __('vendor-module::key') |
|
||||
| [module-identity-settings.md](docs/extension/module-identity-settings.md) | 모듈/플러그인 본인인증(IDV) 설정 통합 가이드 | 정책/목적/메시지: module.php::getIdentity{Policies,Purposes,Mess... |
|
||||
| [module-layouts.md](docs/extension/module-layouts.md) | 모듈 레이아웃 시스템 | 위치: modules/_bundled/vendor-module/resources/layouts/admi... |
|
||||
| [module-routing.md](docs/extension/module-routing.md) | 모듈 라우트 규칙 | URL prefix 자동: /api/admin/[vendor-module]/... |
|
||||
| [module-routing.md](docs/extension/module-routing.md) | 모듈 라우트 규칙 | URL prefix 자동: /api/modules/[vendor-module]/... |
|
||||
| [module-settings.md](docs/extension/module-settings.md) | 모듈 환경설정 시스템 개발 가이드 | - |
|
||||
| [permissions.md](docs/extension/permissions.md) | 권한 시스템 | 구조: User → Role → Permission (기능 레벨) |
|
||||
| [plugin-development.md](docs/extension/plugin-development.md) | 플러그인 개발 가이드 | 디렉토리: plugins/vendor-plugin (예: sirsoft-payment) |
|
||||
@@ -143,6 +144,32 @@
|
||||
| [e2e-testing.md](docs/testing/e2e-testing.md) | 그누보드7 Playwright E2E 테스트 가이드 | - |
|
||||
|
||||
|
||||
### API 레퍼런스 진입점
|
||||
|
||||
> 엔드포인트별 요청 파라미터·응답 필드·요청/응답 예시. 공통 규약(Bearer 토큰 인증, 응답 봉투, 페이지네이션, 401/403/422/428)은 진입점 문서 상단에 정리되어 있다.
|
||||
|
||||
| 대상 | 진입점 | 문서/엔드포인트 |
|
||||
|------|--------|----------------|
|
||||
| 코어 | [docs/backend/api/README.md](docs/backend/api/README.md) | 35 / 291 |
|
||||
|
||||
|
||||
### 확장 API 레퍼런스 (9개 확장, 자동 스캔)
|
||||
|
||||
> 각 확장이 소유하는 API 문서 목차. `php artisan api:docgen` 이 생성하며, 이 표는 `{modules,plugins}/_bundled/*/docs/api/README.md` 를 패턴 스캔해 자동 편입된다(확장명 하드코딩 없음).
|
||||
|
||||
| 확장 | 유형 | API 문서 목차 | 문서/엔드포인트 |
|
||||
|------|------|--------------|----------------|
|
||||
| `gnuboard7-hello_module` | 모듈 | [docs/api/](modules/_bundled/gnuboard7-hello_module/docs/api/README.md) | 1 / 2 |
|
||||
| `sirsoft-board` | 모듈 | [docs/api/](modules/_bundled/sirsoft-board/docs/api/README.md) | 10 / 80 |
|
||||
| `sirsoft-ecommerce` | 모듈 | [docs/api/](modules/_bundled/sirsoft-ecommerce/docs/api/README.md) | 33 / 231 |
|
||||
| `sirsoft-page` | 모듈 | [docs/api/](modules/_bundled/sirsoft-page/docs/api/README.md) | 2 / 17 |
|
||||
| `sirsoft-ckeditor5` | 플러그인 | [docs/api/](plugins/_bundled/sirsoft-ckeditor5/docs/api/README.md) | 2 / 2 |
|
||||
| `sirsoft-gdpr` | 플러그인 | [docs/api/](plugins/_bundled/sirsoft-gdpr/docs/api/README.md) | 4 / 15 |
|
||||
| `sirsoft-marketing` | 플러그인 | [docs/api/](plugins/_bundled/sirsoft-marketing/docs/api/README.md) | 2 / 2 |
|
||||
| `sirsoft-pay_kginicis` | 플러그인 | [docs/api/](plugins/_bundled/sirsoft-pay_kginicis/docs/api/README.md) | 5 / 22 |
|
||||
| `sirsoft-verification_kginicis` | 플러그인 | [docs/api/](plugins/_bundled/sirsoft-verification_kginicis/docs/api/README.md) | 1 / 1 |
|
||||
|
||||
|
||||
<!-- AUTO-GENERATED-END: docs-quick-reference -->
|
||||
|
||||
---
|
||||
@@ -221,11 +248,22 @@
|
||||
|
||||
| 금지 | 올바른 사용 |
|
||||
|------|------------|
|
||||
| `Icon className="w-4 h-4"` | `Icon size="sm"` 또는 `className="text-sm"` |
|
||||
| `Icon className="w-4 h-4"` | `className="text-base"` (아래 등가표) 또는 `size` prop |
|
||||
| `Select valueKey/labelKey` | computed로 `{ value, label }` 변환 |
|
||||
| Form 내 `Button` type 없음 | `type="button"` 명시 (submit 방지) |
|
||||
| `options={{options}}` | `options={{options ?? []}}` (fallback) |
|
||||
|
||||
Icon 은 `<i>` 글리프라 박스 크기가 곧 `font-size` 다. `w-N h-N` 은 박스만 정하고 글리프는 부모 `font-size` 를 상속하므로 어긋난다. 기존 `w-N h-N` 을 옮길 때는 아래 등가표를 쓴다 (Chrome 실측).
|
||||
|
||||
| `w-N h-N` | px | 등가 `className` |
|
||||
|---|---|---|
|
||||
| `w-3 h-3` | 12 | `text-xs` |
|
||||
| `w-3.5 h-3.5` | 14 | `text-sm` |
|
||||
| `w-4 h-4` | 16 | `text-base` |
|
||||
| `w-5 h-5` | 20 | `text-xl` |
|
||||
|
||||
`size` prop 은 Font Awesome `fa-*` 클래스로 매핑되며 등가가 아니다 — `size="sm"` → `fa-sm` → `font-size: 0.875em`(상대값) + `line-height` 붕괴로 16px 이 12.25×0.88px 이 된다. 새 아이콘에는 써도 되지만, 기존 `w-N h-N` 의 치환용으로는 쓰지 않는다.
|
||||
|
||||
### 상태 관리
|
||||
|
||||
| 금지 | 올바른 사용 |
|
||||
@@ -914,7 +952,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) |
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [7.0.3] - 2026-07-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- 화면 구성 파일이 목록을 만들어 내는 과정에서 다국어 이름을 골라 쓸 때, 사이트를 어떤 언어로 보고 있든 항상 한국어 이름이 선택되던 문제를 수정했습니다. 이제 보고 있는 언어의 이름이 표시됩니다. 헤더의 배송국가 선택처럼 화면이 처음 열릴 때 목록을 만들어 두는 곳이 여기에 해당합니다.
|
||||
- 메뉴를 눌러 화면을 이동했을 때, 필요한 정보를 여러 갈래로 나눠 받아오는 화면에서 먼저 도착한 정보가 뒤늦게 도착한 정보에 덮여 사라지는 경우가 있었습니다. 그 탓에 목록이 있어야 할 자리가 간헐적으로 비어 보이고 새로고침하면 정상으로 돌아오곤 했습니다. 이제 어느 정보가 먼저 도착하든 모두 반영됩니다. 이커머스 환경설정의 배송설정 탭에서 배송가능 국가 목록이 비어 보이던 것이 여기에 해당합니다.
|
||||
- 이미 설치를 마친 환경의 설정 파일(`.env`)을 새 서버로 복사해, 데이터베이스가 아직 비어 있는 상태에서 설치를 이어가려 할 때 `php artisan migrate`로 테이블을 만들기도 전에 부팅 단계에서 오류가 나며 중단되던 문제를 수정했습니다. 이제 데이터베이스 작업 명령이 실행되는 동안에는 테이블 존재 여부를 실제로 확인하므로, 기존 서버의 설정을 재사용하는 신규 배포에서도 설치가 정상적으로 진행됩니다. (#41 @GyusoonKim 님께서 제보해주셨습니다.)
|
||||
|
||||
## [7.0.2] - 2026-07-08
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -246,7 +246,7 @@ unzip g7-release.zip
|
||||
|
||||
# 압축 해제 결과 확인 — 루트 디렉토리가 g7이 아니면 이름 변경
|
||||
ls -la
|
||||
# (필요 시) mv g7-7.0.2 g7
|
||||
# (필요 시) mv g7-7.0.3 g7
|
||||
|
||||
# ZIP 파일 정리 (선택)
|
||||
rm g7-release.zip
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#"><img src="https://img.shields.io/badge/version-7.0.2-blue" alt="Version"></a>
|
||||
<a href="#"><img src="https://img.shields.io/badge/version-7.0.3-blue" alt="Version"></a>
|
||||
<a href="#"><img src="https://img.shields.io/badge/PHP-8.2%2B-777BB4?logo=php&logoColor=white" alt="PHP"></a>
|
||||
<a href="#"><img src="https://img.shields.io/badge/Laravel-12.x-FF2D20?logo=laravel&logoColor=white" alt="Laravel"></a>
|
||||
<a href="#"><img src="https://img.shields.io/badge/React-19-61DAFB?logo=react&logoColor=black" alt="React"></a>
|
||||
@@ -16,6 +16,11 @@
|
||||
<a href="#"><img src="https://img.shields.io/badge/status-Stable-brightgreen" alt="Status"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://g7.gnuboard.com"><strong>데모 사이트</strong></a> ·
|
||||
<a href="https://g7.gnuboard.com/admin"><strong>관리자 데모</strong></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
[소개](#그누보드7-소개) · [주요 기능](#주요-기능) · [기술 스택](#기술-스택) · [아키텍처](#아키텍처) · [빠른 시작](#빠른-시작) · [기본 제공 확장](#기본-제공-확장) · [비즈니스 모델](#비즈니스-모델) · [기존 사용자](#기존-그누보드-사용자) · [문서](#문서) · [기여하기](#기여하기) · [만든 사람들](#만든-사람들) · [커뮤니티](#커뮤니티) · [변경 기록](#변경-기록) · [라이선스](#라이선스)
|
||||
@@ -462,7 +467,8 @@ cp .env.example .env
|
||||
| 플러그인 개발 | [docs/extension/plugin-development.md](docs/extension/plugin-development.md) |
|
||||
| 템플릿 개발 | [docs/extension/template-basics.md](docs/extension/template-basics.md) |
|
||||
| 테스트 | [docs/testing-guide.md](docs/testing-guide.md) |
|
||||
| API 레퍼런스 | 준비 중 |
|
||||
| API 레퍼런스 | [docs/backend/api/README.md](docs/backend/api/README.md) |
|
||||
| API 문서 작성 규정 | [docs/backend/api-documentation.md](docs/backend/api-documentation.md) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Support\ApiDoc\ResourceFieldDescriber;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
/**
|
||||
* 기존 API 문서의 응답 필드 설명 TODO 를 리소스 계약 사전 설명으로 소급 채웁니다.
|
||||
*
|
||||
* 파라미터 TODO 는 `api:docgen-backfill-params` 가 채우지만, 응답 필드 TODO 는 전용
|
||||
* 백필 커맨드가 없어 재생성(`api:docgen`)으로만 갱신되었습니다. 실측 서버 없이
|
||||
* 재생성하면 이미 실측된 응답 예시값이 퇴행하므로, 이 커맨드는 재생성 대신 응답 필드
|
||||
* 표의 `<!-- TODO: 설명 -->` 셀만 in-place 치환합니다.
|
||||
*
|
||||
* 채움 규칙 SSoT 는 ApiDocScaffolder::responseFieldTable 과 동일한 ResourceFieldDescriber
|
||||
* 입니다 — 이후 정상 재생성(실측 서버 가동 시)도 같은 설명을 산출하므로 멱등합니다.
|
||||
*
|
||||
* 도메인 특이 필드(ResourceFieldDescriber 가 null 반환)는 TODO 를 그대로 둡니다.
|
||||
* 응답 필드 표만 대상으로 하며, 파라미터 표(`<!-- 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 = '<!-- TODO: 설명 -->';
|
||||
|
||||
/**
|
||||
* 커맨드를 실행합니다.
|
||||
*
|
||||
* @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<int, string> 파일 경로 목록
|
||||
*/
|
||||
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 를 채웁니다.
|
||||
*
|
||||
* 응답 필드 표 헤더 이후 표 행만 대상으로 하며, `<!-- 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` | <!-- TODO: 설명 --> |
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Support\ApiDoc\ParameterDescriber;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
/**
|
||||
* 기존 API 문서의 파라미터 용도 TODO 를 공통 파라미터 설명으로 소급 채웁니다.
|
||||
*
|
||||
* 실측 서버 없이 재생성하면 이미 실측된 응답표·사람 서술이 퇴행하므로, 이 커맨드는
|
||||
* 재생성 대신 파라미터 표의 `<!-- TODO: 용도 -->` 셀만 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 = '<!-- TODO: 용도 -->';
|
||||
|
||||
/**
|
||||
* 커맨드를 실행합니다.
|
||||
*
|
||||
* @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<int, string> 파일 경로 목록
|
||||
*/
|
||||
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 를 채웁니다.
|
||||
*
|
||||
* 파라미터 표 헤더 이후 표 행만 대상으로 하며, `<!-- 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 | 필수 | 허용값 | <!-- TODO: 용도 --> |
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,937 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Contracts\ApiDoc\ApiDocSampleSeeder;
|
||||
use App\Models\Module;
|
||||
use App\Models\Plugin;
|
||||
use App\Models\Template;
|
||||
use App\Models\User;
|
||||
use App\Support\ApiDoc\ApiDocSampleService;
|
||||
use App\Support\ApiDoc\ApiDocScaffolder;
|
||||
use App\Support\ApiDoc\ApiEndpointProbe;
|
||||
use App\Support\ApiDoc\ApiRouteInventory;
|
||||
use App\Support\ApiDoc\ColumnCommentResolver;
|
||||
use App\Support\ApiDoc\FormRequestIntrospector;
|
||||
use App\Support\ApiDoc\ResponseSchemaInferrer;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* API 레퍼런스 문서 생성 커맨드
|
||||
*
|
||||
* 등록된 API 라우트를 전수 수집하고, GET 엔드포인트는 실제 HTTP 호출로 응답을
|
||||
* 실측하여 요청 파라미터·응답 필드를 담은 마크다운 레퍼런스 문서를 생성합니다.
|
||||
*
|
||||
* @generated 블록만 갱신하며 사람이 작성한 서술은 보존합니다.
|
||||
*/
|
||||
class ApiDocgenCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var string 커맨드 시그니처
|
||||
*/
|
||||
protected $signature = 'api:docgen
|
||||
{--scope=core : 범위 (core, module:vendor-id, plugin:vendor-id, all)}
|
||||
{--base-url= : 실측 기준 URL (미지정 시 .env APP_URL)}
|
||||
{--user= : 실측 토큰 발급 대상 사용자 ID}
|
||||
{--seed : 실측 전 완전 샘플 데이터를 시드 (개발 환경 전용)}
|
||||
{--check : 생성하지 않고 누락/미실측만 리포트}
|
||||
{--examples-only : 기존 문서에 요청/응답 예시 블록만 in-place 삽입 (표·서술 불가침, 재생성 금지)}
|
||||
{--dry-run : 생성 대상만 출력}';
|
||||
|
||||
/**
|
||||
* @var string 커맨드 설명
|
||||
*/
|
||||
protected $description = 'API 레퍼런스 문서를 실측 기반으로 생성/갱신합니다';
|
||||
|
||||
/**
|
||||
* 커맨드를 실행합니다.
|
||||
*
|
||||
* @param ApiRouteInventory $inventory 라우트 인벤토리
|
||||
* @param FormRequestIntrospector $introspector FormRequest 분석기
|
||||
* @param ResponseSchemaInferrer $inferrer 응답 스키마 추론기
|
||||
* @param ApiDocScaffolder $scaffolder 스캐폴딩 생성기
|
||||
* @param ColumnCommentResolver $commentResolver 컬럼 주석 해석기
|
||||
* @return int 종료 코드
|
||||
*/
|
||||
public function handle(
|
||||
ApiRouteInventory $inventory,
|
||||
FormRequestIntrospector $introspector,
|
||||
ResponseSchemaInferrer $inferrer,
|
||||
ApiDocScaffolder $scaffolder,
|
||||
ColumnCommentResolver $commentResolver
|
||||
): int {
|
||||
$scope = (string) $this->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, 'examples' => 0];
|
||||
$checkFindings = [];
|
||||
$examplesOnly = (bool) $this->option('examples-only');
|
||||
|
||||
// 대상(코어/확장)별 README 목차 항목 누적: readmeKey => ['label' => ..., 'entries' => [...]]
|
||||
$readmeIndex = [];
|
||||
|
||||
foreach ($grouped as $file => $items) {
|
||||
$sections = [];
|
||||
$sectionKeys = [];
|
||||
$exampleBlocks = [];
|
||||
|
||||
// 목차용 도메인 파일 항목 (소유별). 예시-only/check 모드에서도 목차는 최신화한다.
|
||||
$owner = $items[0]['owner'];
|
||||
$readmeKey = $this->readmeFile($owner);
|
||||
$readmeIndex[$readmeKey]['label'] = $owner['type'] === 'core'
|
||||
? '코어'
|
||||
: ($owner['type'] === 'module' ? '모듈' : '플러그인')." `{$owner['id']}`";
|
||||
$readmeIndex[$readmeKey]['entries'][] = [
|
||||
'domain' => $items[0]['domain_group'],
|
||||
'file' => basename($file),
|
||||
'count' => count($items),
|
||||
];
|
||||
|
||||
foreach ($items as $route) {
|
||||
$request = $introspector->introspect($route['controller'], $route['controller_method']);
|
||||
[$schema, $probeMeta] = $this->probeEndpoint($probe, $inferrer, $route, $probed, $sampleMap, $request);
|
||||
|
||||
if ($probeMeta['skipped_reason'] === null) {
|
||||
$stats['probed']++;
|
||||
} else {
|
||||
$stats['skipped']++;
|
||||
$checkFindings[] = "{$route['method']} {$route['uri']} — {$probeMeta['skipped_reason']}";
|
||||
}
|
||||
|
||||
$key = $route['name'] ?: $route['uri'];
|
||||
|
||||
if ($examplesOnly) {
|
||||
// 표·서술을 건드리지 않고 예시 2블록만 산출 (SSoT=스캐폴더 예시 메서드).
|
||||
// 단, 응답 필드 표가 아직 실측 제외 마커(사람이 채운 셀 없음)인데 이번 실측으로
|
||||
// 스키마가 확보됐으면 그 마커를 실측 필드 표로 갱신한다(쓰기 메서드 실측 도입분).
|
||||
$commentMap = $this->columnComments($route, $commentResolver, $sampleMap);
|
||||
$exampleBlocks[$key] = [
|
||||
'request' => $scaffolder->requestExampleBlock($route, $request, $probeMeta),
|
||||
'response' => $scaffolder->responseExampleBlock($schema, $probeMeta),
|
||||
'response_fields' => $schema !== null
|
||||
? $scaffolder->responseFieldTableBlock($schema, $probeMeta, $commentMap)
|
||||
: null,
|
||||
];
|
||||
$stats['endpoints']++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$commentMap = $this->columnComments($route, $commentResolver, $sampleMap);
|
||||
$sections[] = $scaffolder->endpointSection($route, $request, $schema, $probeMeta, $commentMap);
|
||||
$sectionKeys[] = $key;
|
||||
$stats['endpoints']++;
|
||||
}
|
||||
|
||||
if ($this->option('check')) {
|
||||
if (! File::exists($file)) {
|
||||
$checkFindings[] = "문서 파일 없음: {$file}";
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// examples-only: 재생성 없이 기존 문서에 예시 블록만 in-place 삽입.
|
||||
// 전체 재생성은 @generated 블록 내부 표(파라미터/응답 필드)의 사람 서술을
|
||||
// TODO 로 되돌리므로, 예시 방출을 위해 재생성을 쓰지 않는다.
|
||||
if ($examplesOnly) {
|
||||
if (! File::exists($file)) {
|
||||
$this->warn("문서 없음(예시 삽입 건너뜀): {$file}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = File::get($file);
|
||||
[$updated, $inserted] = $scaffolder->insertExampleBlocks($existing, $exampleBlocks);
|
||||
|
||||
if ($updated !== $existing) {
|
||||
File::put($file, $updated);
|
||||
$stats['files']++;
|
||||
}
|
||||
$stats['examples'] += $inserted;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$header = $this->documentHeader($file, $items[0]);
|
||||
$existing = File::exists($file) ? File::get($file) : null;
|
||||
$content = $scaffolder->mergeDocument($existing, $header, $sections, $sectionKeys);
|
||||
|
||||
File::ensureDirectoryExists(dirname($file));
|
||||
File::put($file, $content);
|
||||
$stats['files']++;
|
||||
}
|
||||
|
||||
$probe->cleanup();
|
||||
|
||||
// 대상별 API 문서 목차(README.md) 생성/갱신 — 확장 API 레퍼런스 발견성의 규약 진입점.
|
||||
// check/dry-run 은 파일을 쓰지 않으므로 제외. examples-only 는 목차도 최신화한다.
|
||||
if (! $this->option('check')) {
|
||||
$coreReadme = $this->readmeFile(['type' => 'core', 'id' => null]);
|
||||
|
||||
foreach ($readmeIndex as $readmePath => $meta) {
|
||||
$existing = File::exists($readmePath) ? File::get($readmePath) : null;
|
||||
// 코어 README 는 최상위 README 의 "API 레퍼런스" 진입점이므로 확장 목차를 함께 싣는다.
|
||||
$extensions = $readmePath === $coreReadme ? $this->scanExtensionApiReadmes() : null;
|
||||
$content = $scaffolder->readmeIndex($meta['label'], $meta['entries'], $existing, $extensions);
|
||||
File::ensureDirectoryExists(dirname($readmePath));
|
||||
File::put($readmePath, $content);
|
||||
}
|
||||
|
||||
// 확장 스코프 실행이라 코어 README 를 다시 쓰지 않았어도, 그 확장의 엔드포인트 수가
|
||||
// 바뀌었을 수 있으므로 코어 README 의 확장 목차 블록만 in-place 갱신한다.
|
||||
if (! array_key_exists($coreReadme, $readmeIndex) && File::exists($coreReadme)) {
|
||||
$updated = $scaffolder->refreshExtensionIndex(
|
||||
File::get($coreReadme),
|
||||
$this->scanExtensionApiReadmes()
|
||||
);
|
||||
if ($updated !== null) {
|
||||
File::put($coreReadme, $updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if ($examplesOnly) {
|
||||
$this->info(sprintf(
|
||||
'예시 삽입 완료: 파일 %d개, 예시 블록 %d개 (실측 %d, 제외 %d)',
|
||||
$stats['files'],
|
||||
$stats['examples'],
|
||||
$stats['probed'],
|
||||
$stats['skipped']
|
||||
));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info(sprintf(
|
||||
'완료: 파일 %d개, 엔드포인트 %d개 (실측 %d, 제외 %d)',
|
||||
$stats['files'],
|
||||
$stats['endpoints'],
|
||||
$stats['probed'],
|
||||
$stats['skipped']
|
||||
));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 수집된 라우트의 소유 확장에서 규약 위치의 샘플 시더를 발견합니다.
|
||||
*
|
||||
* 확장 컨트롤러 FQCN(`Modules\Vendor\Ext\Http\Controllers\...`)에서 확장 베이스
|
||||
* 네임스페이스를 추출하고, `{Base}\Support\ApiDoc\ApiDocSampleService` 규약 클래스가
|
||||
* 존재하며 ApiDocSampleSeeder 를 구현하면 인스턴스를 반환합니다. 각 확장은 1회만 처리합니다.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $routes 수집된 라우트 목록
|
||||
* @return array<string, ApiDocSampleSeeder> 확장 라벨(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<string, mixed> $route 라우트 메타데이터
|
||||
* @param bool $probed 실측 가능 여부
|
||||
* @param array<string, array{model: class-string, key: string, value: string}> $sampleMap 도메인별 대표 샘플 맵
|
||||
* @param array<string, mixed> $request FormRequest 분석 결과 (쓰기 바디 구성용)
|
||||
* @return array{0: array<string, mixed>|null, 1: array<string, mixed>} 스키마와 실측 메타
|
||||
*/
|
||||
private function probeEndpoint(ApiEndpointProbe $probe, ResponseSchemaInferrer $inferrer, array $route, bool $probed, array $sampleMap = [], array $request = []): array
|
||||
{
|
||||
$baseUrl = $probe->baseUrl();
|
||||
|
||||
if (! $probed) {
|
||||
return [null, ['status' => null, 'skipped_reason' => 'no-token', 'base_url' => $baseUrl, 'resolved_uri' => null, 'body' => null]];
|
||||
}
|
||||
|
||||
$uri = $this->resolvePathParams($route, $sampleMap);
|
||||
$isWrite = in_array($route['method'], ['POST', 'PUT', 'PATCH', 'DELETE'], true);
|
||||
|
||||
// 요청 예시에 노출할 정규 URI (실측용 per_page 보정 제외).
|
||||
$exampleUri = $this->exampleUri($route, $sampleMap);
|
||||
|
||||
// 부수효과가 트랜잭션 롤백으로 되돌릴 수 없는 쓰기(확장 install/activate/update,
|
||||
// 언어팩 설치, 코어 업데이트, 파일 업로드 등)는 in-process 실측에서 제외한다.
|
||||
// 이들은 파일시스템·프로세스·외부 네트워크를 건드려 롤백 불가·hang 위험이 있다.
|
||||
if ($isWrite && $this->isSideEffectfulWrite($route)) {
|
||||
return [null, [
|
||||
'status' => null,
|
||||
'skipped_reason' => 'side-effectful-write',
|
||||
'base_url' => $baseUrl,
|
||||
'resolved_uri' => $exampleUri,
|
||||
'body' => null,
|
||||
]];
|
||||
}
|
||||
|
||||
// 쓰기 메서드는 FormRequest 규칙 기반 실측 바디를 만들어 in-process 롤백 실측에 사용한다.
|
||||
$writeBody = in_array($route['method'], ['POST', 'PUT', 'PATCH'], true)
|
||||
? $this->buildWriteBody($request, $sampleMap)
|
||||
: [];
|
||||
$result = $probe->probe($route['method'], $uri, $writeBody);
|
||||
|
||||
if (! $result['ok'] || $result['body'] === null) {
|
||||
return [null, [
|
||||
'status' => $result['status'],
|
||||
'skipped_reason' => $result['skipped_reason'] ?? ('http-'.$result['status']),
|
||||
'base_url' => $baseUrl,
|
||||
'resolved_uri' => $exampleUri,
|
||||
'body' => null,
|
||||
]];
|
||||
}
|
||||
|
||||
return [$inferrer->infer($result['body']), [
|
||||
'status' => $result['status'],
|
||||
'skipped_reason' => null,
|
||||
'base_url' => $baseUrl,
|
||||
'resolved_uri' => $exampleUri,
|
||||
'body' => $result['body'],
|
||||
]];
|
||||
}
|
||||
|
||||
/**
|
||||
* 부수효과가 트랜잭션 롤백으로 되돌릴 수 없는 쓰기 엔드포인트인지 판별합니다.
|
||||
*
|
||||
* 확장(모듈/플러그인/템플릿) 및 언어팩 설치·활성·업데이트, 코어 업데이트, 파일 업로드,
|
||||
* 캐시/워밍업/생성 등 파일시스템·프로세스·외부 네트워크를 건드리는 쓰기는 in-process 롤백
|
||||
* 실측이 불가능(hang·비가역 부수효과)하므로 실측에서 제외한다. URI 세그먼트로 판정한다.
|
||||
*
|
||||
* @param array<string, mixed> $route 라우트 메타데이터
|
||||
* @return bool 부수효과 쓰기 여부
|
||||
*/
|
||||
private function isSideEffectfulWrite(array $route): bool
|
||||
{
|
||||
$uri = (string) $route['uri'];
|
||||
|
||||
// 확장/언어팩 관리 도메인의 쓰기는 전량 제외 (install/activate/... 은 실환경 변경).
|
||||
if (preg_match('#/api/admin/(modules|plugins|templates|language-packs|core-update)(/|$)#', $uri)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 동작성 세그먼트를 가진 쓰기 (도메인 무관).
|
||||
$unsafeSegments = [
|
||||
'install', 'install-from-file', 'install-from-github', 'install-from-url', 'install-from-bundled',
|
||||
'uninstall', 'activate', 'deactivate', 'bulk-activate', 'update', 'check-updates',
|
||||
'refresh-cache', 'clear', 'warmup', 'generate', 'sync', 'backup', 'restore',
|
||||
'upload', 'import', 'export', 'manifest-preview', 'preview',
|
||||
];
|
||||
|
||||
foreach ($unsafeSegments as $seg) {
|
||||
if (str_contains($uri, '/'.$seg)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청 예시에 노출할 정규 URI 를 산출합니다.
|
||||
*
|
||||
* `resolvePathParams` 는 목록 GET 실측용으로 `?per_page=25` 를 덧붙이지만,
|
||||
* 요청 예시에는 이 실측 보정을 제외한 순수 경로(치환된 path 파라미터 포함)를 노출한다.
|
||||
* path 파라미터가 치환되지 않은(실측 제외) 경우 `{param}` placeholder 를 그대로 남긴다.
|
||||
*
|
||||
* @param array<string, mixed> $route 라우트 메타데이터
|
||||
* @param array<string, array{model: class-string, key: string, value: string}> $sampleMap 도메인별 대표 샘플 맵
|
||||
* @return string 요청 예시용 URI
|
||||
*/
|
||||
private function exampleUri(array $route, array $sampleMap): string
|
||||
{
|
||||
$resolved = $this->resolvePathParams($route, $sampleMap);
|
||||
|
||||
// 실측용 per_page 보정 쿼리를 제거해 정규 경로만 남긴다.
|
||||
return preg_replace('/[?&]per_page=25$/', '', $resolved) ?? $resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* 쓰기 메서드 in-process 실측용 요청 바디를 FormRequest 규칙 근거로 구성합니다.
|
||||
*
|
||||
* 필수 파라미터를 우선하고, 검증을 통과할 수 있도록 이름·타입·허용값에 맞는 유효값을 채웁니다.
|
||||
* 파일(image/file) 파라미터는 실측 바디에서 제외한다(in-process dispatch 로 파일 전송 불가 →
|
||||
* 해당 엔드포인트는 http-422 로 스킵되고 정적 요청 예시[multipart]만 방출된다).
|
||||
*
|
||||
* @param array<string, mixed> $request FormRequest 분석 결과
|
||||
* @param array<string, array{model: class-string, key: string, value: string}> $sampleMap 도메인별 대표 샘플 맵
|
||||
* @return array<string, mixed> 실측 요청 바디
|
||||
*/
|
||||
private function buildWriteBody(array $request, array $sampleMap = []): array
|
||||
{
|
||||
$params = $request['params'] ?? [];
|
||||
$body = [];
|
||||
|
||||
foreach ($params as $p) {
|
||||
$name = (string) $p['name'];
|
||||
$type = (string) ($p['type'] ?? 'string');
|
||||
|
||||
// 파일 타입은 in-process 바디로 전송 불가 → 생략.
|
||||
if (in_array($type, ['file', 'image'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$body[$name] = $this->writeValue($name, $type, (string) ($p['allowed'] ?? ''), $sampleMap);
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 쓰기 실측 바디의 파라미터 값을 검증 통과 가능하도록 생성합니다.
|
||||
*
|
||||
* @param string $name 파라미터명
|
||||
* @param string $type 타입
|
||||
* @param string $allowed 허용값 설명 (in: 열거 등)
|
||||
* @param array<string, array{model: class-string, key: string, value: string}> $sampleMap 도메인 샘플 맵
|
||||
* @return mixed 유효 예시값
|
||||
*/
|
||||
private function writeValue(string $name, string $type, string $allowed, array $sampleMap): mixed
|
||||
{
|
||||
$lower = strtolower($name);
|
||||
|
||||
// 허용값 in: 열거(백틱)가 있으면 첫 값을 채택(검증 통과 보장).
|
||||
if (preg_match('/`([^`]+)`/', $allowed, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
|
||||
return match (true) {
|
||||
$type === 'boolean' => true,
|
||||
$type === 'integer', $type === 'number' => 1,
|
||||
$type === 'array' => [],
|
||||
$type === 'email' || str_contains($lower, 'email') => 'probe_'.uniqid().'@example.com',
|
||||
$type === 'uuid' => (string) Str::uuid(),
|
||||
$type === 'date' => '2026-01-01',
|
||||
str_contains($lower, 'password') => 'Password123!',
|
||||
str_contains($lower, 'url') || str_contains($lower, 'homepage') => 'https://example.com',
|
||||
// identifier/slug 는 소문자/숫자/밑줄만 허용하는 규칙이 흔하므로 하이픈 없이 생성.
|
||||
str_contains($lower, 'identifier') || str_contains($lower, 'slug') => 'probe_'.uniqid(),
|
||||
str_contains($lower, 'locale') || $lower === 'language' => 'ko',
|
||||
str_contains($lower, 'country') => 'KR',
|
||||
str_contains($lower, 'timezone') => 'Asia/Seoul',
|
||||
default => '실측 예시값',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* URI 의 path 파라미터를 바인딩된 모델의 실제 레코드로 치환합니다.
|
||||
*
|
||||
* @param array<string, mixed> $route 라우트 메타데이터
|
||||
* @param array<string, array{model: class-string, key: string, value: string}> $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-model binding 없이 `{id}`/`{identifier}`/`{key}` 문자열
|
||||
// 파라미터를 쓰고 컨트롤러가 직접 조회한다. 도메인 대표 샘플 모델의 route key(또는
|
||||
// 해당 컬럼)로 치환해 실측 가능하게 한다. (마지막 세그먼트 리소스 파라미터에 한함)
|
||||
$generic = $this->resolveGenericPathParam($param, $domain, $sampleMap);
|
||||
|
||||
if ($generic !== null) {
|
||||
$uri = str_replace('{'.$param.'}', (string) $generic, $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* route-model binding 없는 제네릭 path 파라미터를 도메인 샘플 모델의 실제 키로 치환합니다.
|
||||
*
|
||||
* 코어 다수 라우트는 `{id}`/`{identifier}`/`{key}`/`{slug}` 문자열 파라미터를 쓰고 컨트롤러가
|
||||
* 직접 조회한다. 도메인 대표 샘플 모델을 찾아 파라미터명에 대응하는 컬럼값으로 치환한다:
|
||||
* - `id` → 모델 기본키
|
||||
* - `identifier`/`slug`/`key`/`code` → 같은 이름 컬럼이 있으면 그 값, 없으면 route key
|
||||
* - 그 외 → 치환 안 함(null 반환 → 실측 제외 유지, 오치환 방지)
|
||||
*
|
||||
* @param string $param path 파라미터명
|
||||
* @param string|null $domain 도메인 그룹명
|
||||
* @param array<string, array{model: class-string, key: string, value: string}> $sampleMap 도메인 샘플 맵
|
||||
* @return string|null 치환값 (없으면 null)
|
||||
*/
|
||||
private function resolveGenericPathParam(string $param, ?string $domain, array $sampleMap): ?string
|
||||
{
|
||||
$modelClass = ($domain !== null && isset($sampleMap[$domain]))
|
||||
? $sampleMap[$domain]['model']
|
||||
: $this->domainModelHint($domain);
|
||||
|
||||
if (! $modelClass) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var Model $model */
|
||||
$model = new $modelClass;
|
||||
$record = $modelClass::query()->orderBy($model->getKeyName())->first();
|
||||
|
||||
if (! $record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lower = strtolower($param);
|
||||
|
||||
// id 계열 → 기본키
|
||||
if ($lower === 'id' || str_ends_with($lower, '_id')) {
|
||||
return (string) $record->getKey();
|
||||
}
|
||||
|
||||
// identifier/slug/key/code → 동일 컬럼이 있으면 그 값
|
||||
if (in_array($lower, ['identifier', 'slug', 'key', 'code'], true)) {
|
||||
$val = $record->getAttribute($lower);
|
||||
|
||||
return $val !== null ? (string) $val : (string) $record->getRouteKey();
|
||||
}
|
||||
|
||||
// 도메인 단수 리소스명 파라미터 → route key
|
||||
if ($domain !== null && $this->paramMatchesDomain($param, $domain)) {
|
||||
return (string) $record->getRouteKey();
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* path 파라미터명이 도메인의 단수 리소스명과 일치하는지 판정합니다.
|
||||
*
|
||||
* 도메인 그룹은 복수형(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<string, mixed> $route 라우트 메타데이터
|
||||
* @param ColumnCommentResolver $resolver 컬럼 주석 해석기
|
||||
* @param array<string, array{model: class-string, key: string, value: string}> $sampleMap 도메인별 대표 샘플 맵
|
||||
* @return array<string, string> 컬럼명 => 주석
|
||||
*/
|
||||
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<string, array{model: class-string, key: string, value: string}> $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<string, array{model: class-string, key: string, value: string}> $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<string, mixed> $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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 번들 확장이 소유한 API 문서 목차(README.md)를 파일 시스템 패턴 스캔으로 수집합니다.
|
||||
*
|
||||
* 확장명을 하드코딩하지 않고 `{modules,plugins}/_bundled/{*}/docs/api/README.md` 존재
|
||||
* 여부만으로 발견한다(동적 로딩 원칙). 문서 수·엔드포인트 수는 스캐폴더가 방출한
|
||||
* 집계 라인(`**문서 수**: N · **엔드포인트 수**: M`)에서 읽는다.
|
||||
*
|
||||
* 링크 경로는 코어 README(`docs/backend/api/`) 기준 상대경로로 만든다.
|
||||
*
|
||||
* @return array<int, array{id: string, type: string, path: string, docs: int, endpoints: int}> 확장 목록
|
||||
*/
|
||||
private function scanExtensionApiReadmes(): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach (['modules' => 'module', 'plugins' => 'plugin'] as $dir => $type) {
|
||||
$bundled = base_path("{$dir}/_bundled");
|
||||
if (! File::isDirectory($bundled)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (File::directories($bundled) as $extDir) {
|
||||
$readme = $extDir.DIRECTORY_SEPARATOR.'docs'.DIRECTORY_SEPARATOR.'api'.DIRECTORY_SEPARATOR.'README.md';
|
||||
if (! File::exists($readme)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = basename($extDir);
|
||||
$content = File::get($readme);
|
||||
$docs = 0;
|
||||
$endpoints = 0;
|
||||
if (preg_match('/\*\*문서 수\*\*:\s*(\d+)\s*·\s*\*\*엔드포인트 수\*\*:\s*(\d+)/u', $content, $m)) {
|
||||
$docs = (int) $m[1];
|
||||
$endpoints = (int) $m[2];
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'id' => $id,
|
||||
'type' => $type,
|
||||
// docs/backend/api/README.md → 프로젝트 루트까지 3단계 상위
|
||||
'path' => "../../../{$dir}/_bundled/{$id}/docs/api/README.md",
|
||||
'docs' => $docs,
|
||||
'endpoints' => $endpoints,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 소유 대상의 API 문서 목차(README.md) 경로를 반환합니다.
|
||||
*
|
||||
* @param array{type: string, id: string|null} $owner 소유 메타
|
||||
* @return string README.md 절대 경로
|
||||
*/
|
||||
private function readmeFile(array $owner): string
|
||||
{
|
||||
if ($owner['type'] === 'core') {
|
||||
return base_path('docs/backend/api/README.md');
|
||||
}
|
||||
|
||||
$base = $owner['type'] === 'module' ? 'modules' : 'plugins';
|
||||
|
||||
return base_path("{$base}/_bundled/{$owner['id']}/docs/api/README.md");
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 헤더(제목 + TL;DR)를 생성합니다.
|
||||
*
|
||||
* @param string $file 문서 파일 경로
|
||||
* @param array<string, mixed> $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 <<<MD
|
||||
# {$domain} API 레퍼런스
|
||||
|
||||
> **소유**: {$ownerLabel} · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 {$domain} 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
MD;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts\ApiDoc;
|
||||
|
||||
/**
|
||||
* API 문서 실측용 완전 샘플 시더 계약
|
||||
*
|
||||
* 확장(모듈/플러그인)이 자신의 API 도메인 대표 엔티티에 완전한 샘플 레코드를
|
||||
* 멱등하게 생성하고, docgen 이 상세 GET 실측(path 파라미터 치환)에 사용할
|
||||
* 도메인별 대표 route key 맵을 제공하기 위한 인터페이스입니다.
|
||||
*
|
||||
* 확장은 이 인터페이스를 구현한 클래스를 `{Namespace}\Support\ApiDoc\ApiDocSampleService`
|
||||
* 규약 위치에 두며, `api:docgen --scope=module:{id}` 실행 시 커맨드가 자동으로 발견합니다.
|
||||
* 코어의 `App\Support\ApiDoc\ApiDocSampleService` 도 동일 계약을 구현합니다.
|
||||
*/
|
||||
interface ApiDocSampleSeeder
|
||||
{
|
||||
/**
|
||||
* 도메인별 완전 샘플을 멱등 생성하고 대표 route key 맵을 반환합니다.
|
||||
*
|
||||
* 반환 맵의 키는 라우트 도메인 그룹명(`domain_group`, 예: pages)이며,
|
||||
* 값은 그 도메인의 상세 GET path 파라미터를 실측 가능한 실제 레코드로
|
||||
* 치환하기 위한 모델 FQCN·route key 이름·route key 값입니다.
|
||||
*
|
||||
* 선택 필드 `path_params`(param 명 => 실제 값 맵)를 두면, route-model binding
|
||||
* 도 도메인 폴백도 없는 문자열 path 파라미터(예: 게시판 slug 라우팅
|
||||
* `boards/{slug}/posts/{id}`)를 param 명 정확 일치로 치환해 상세 GET 을
|
||||
* 실측할 수 있습니다. 미제공 시 route key 기반 자동 치환만 적용됩니다.
|
||||
*
|
||||
* @return array<string, array{model: class-string, key: string, value: string, path_params?: array<string, string>}> 도메인 => 대표 레코드 정보
|
||||
*/
|
||||
public function seed(): array;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Contracts\Extension\CacheInterface;
|
||||
use App\Enums\ExtensionStatus;
|
||||
use App\Extension\Cache\CoreCacheDriver;
|
||||
use App\Models\Module;
|
||||
use App\Support\InstallerContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
@@ -63,8 +64,6 @@ trait CachesModuleStatus
|
||||
/**
|
||||
* 모듈 상태 캐시를 무효화합니다.
|
||||
* 모듈 상태 변경 시 (install, activate, deactivate, uninstall) 호출해야 합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function invalidateModuleStatusCache(): void
|
||||
{
|
||||
@@ -79,10 +78,14 @@ trait CachesModuleStatus
|
||||
* 설치 완료 상태(`config('app.installer_completed')`)일 때는 테이블 존재를
|
||||
* 전제로 하여 `Schema::hasTable()` 호출을 건너뜁니다. 인스톨러 이전 환경이나
|
||||
* 테스트에서는 기존 체크 경로로 폴백합니다.
|
||||
*
|
||||
* 단, 마이그레이션 계열 명령 실행 중에는 `INSTALLER_COMPLETED=true` 라도 테이블이
|
||||
* 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
|
||||
* 신뢰하지 않고 실제 `Schema::hasTable()` 폴백으로 진입합니다.
|
||||
*/
|
||||
private static function isExtensionTableReady(string $table): bool
|
||||
{
|
||||
if (config('app.installer_completed')) {
|
||||
if (! InstallerContext::isSchemaMutatingCommand() && config('app.installer_completed')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Contracts\Extension\CacheInterface;
|
||||
use App\Enums\ExtensionStatus;
|
||||
use App\Extension\Cache\CoreCacheDriver;
|
||||
use App\Models\Plugin;
|
||||
use App\Support\InstallerContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
@@ -63,8 +64,6 @@ trait CachesPluginStatus
|
||||
/**
|
||||
* 플러그인 상태 캐시를 무효화합니다.
|
||||
* 플러그인 상태 변경 시 (install, activate, deactivate, uninstall) 호출해야 합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function invalidatePluginStatusCache(): void
|
||||
{
|
||||
@@ -76,10 +75,14 @@ trait CachesPluginStatus
|
||||
/**
|
||||
* 설치 완료 상태에서는 `Schema::hasTable()` 호출을 건너뜁니다.
|
||||
* 인스톨러 이전 환경이나 테스트에서는 기존 체크 경로로 폴백합니다.
|
||||
*
|
||||
* 단, 마이그레이션 계열 명령 실행 중에는 `INSTALLER_COMPLETED=true` 라도 테이블이
|
||||
* 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
|
||||
* 신뢰하지 않고 실제 `Schema::hasTable()` 폴백으로 진입합니다.
|
||||
*/
|
||||
private static function isPluginTableReady(): bool
|
||||
{
|
||||
if (config('app.installer_completed')) {
|
||||
if (! InstallerContext::isSchemaMutatingCommand() && config('app.installer_completed')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Contracts\Extension\CacheInterface;
|
||||
use App\Enums\ExtensionStatus;
|
||||
use App\Extension\Cache\CoreCacheDriver;
|
||||
use App\Models\Template;
|
||||
use App\Support\InstallerContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
@@ -86,8 +87,6 @@ trait CachesTemplateStatus
|
||||
/**
|
||||
* 템플릿 상태 캐시를 무효화합니다.
|
||||
* 템플릿 상태 변경 시 (install, activate, deactivate, uninstall) 호출해야 합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function invalidateTemplateStatusCache(): void
|
||||
{
|
||||
@@ -101,10 +100,14 @@ trait CachesTemplateStatus
|
||||
/**
|
||||
* 설치 완료 상태에서는 `Schema::hasTable()` 호출을 건너뜁니다.
|
||||
* 인스톨러 이전 환경이나 테스트에서는 기존 체크 경로로 폴백합니다.
|
||||
*
|
||||
* 단, 마이그레이션 계열 명령 실행 중에는 `INSTALLER_COMPLETED=true` 라도 테이블이
|
||||
* 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
|
||||
* 신뢰하지 않고 실제 `Schema::hasTable()` 폴백으로 진입합니다.
|
||||
*/
|
||||
private static function isTemplateTableReady(): bool
|
||||
{
|
||||
if (config('app.installer_completed')) {
|
||||
if (! InstallerContext::isSchemaMutatingCommand() && config('app.installer_completed')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ use App\Services\LanguagePack\LanguagePackBundledRegistrar;
|
||||
use App\Services\LanguagePack\LanguagePackRegistry;
|
||||
use App\Services\LanguagePack\LanguagePackSeedInjector;
|
||||
use App\Services\LanguagePack\LanguagePackTranslator;
|
||||
use App\Support\InstallerContext;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\File;
|
||||
@@ -477,8 +478,7 @@ class LanguagePackServiceProvider extends ServiceProvider
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->app->runningInConsole()
|
||||
&& in_array($_SERVER['argv'][1] ?? null, ['migrate', 'migrate:fresh', 'migrate:rollback', 'migrate:reset', 'db:wipe'], true)) {
|
||||
if (InstallerContext::isSchemaMutatingCommand()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Enums\ExtensionStatus;
|
||||
use App\Extension\ExtensionManager;
|
||||
use App\Extension\Testing\ExtensionTestAllowlist;
|
||||
use App\Models\Module;
|
||||
use App\Support\InstallerContext;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -52,7 +53,12 @@ class ModuleRouteServiceProvider extends ServiceProvider
|
||||
|
||||
// 설치 완료 상태에서는 Schema introspection 을 건너뜀 (매 요청 쿼리 제거).
|
||||
// 인스톨러 이전 환경에서는 기존 체크 경로로 폴백.
|
||||
if (! config('app.installer_completed')) {
|
||||
//
|
||||
// 단, 마이그레이션 계열 명령 실행 중에는 INSTALLER_COMPLETED=true 라도 테이블이
|
||||
// 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
|
||||
// 무력화하고 hasTable 검증 경로로 진입한다. 빈 DB 면 여기서 early return 하여
|
||||
// 아래 무방비 pluck 이 table not found 로 부팅을 깨뜨리는 것을 막는다.
|
||||
if (! config('app.installer_completed') || InstallerContext::isSchemaMutatingCommand()) {
|
||||
try {
|
||||
if (! Schema::hasTable('modules')) {
|
||||
return;
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Providers;
|
||||
|
||||
use App\Extension\ExtensionManager;
|
||||
use App\Extension\Testing\ExtensionTestAllowlist;
|
||||
use App\Support\InstallerContext;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -48,7 +49,12 @@ class PluginRouteServiceProvider extends ServiceProvider
|
||||
|
||||
// 설치 완료 상태에서는 Schema introspection 을 건너뜀 (매 요청 쿼리 제거).
|
||||
// 인스톨러 이전 환경에서는 기존 체크 경로로 폴백.
|
||||
if (! config('app.installer_completed')) {
|
||||
//
|
||||
// 단, 마이그레이션 계열 명령 실행 중에는 INSTALLER_COMPLETED=true 라도 테이블이
|
||||
// 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
|
||||
// 무력화하고 hasTable 검증 경로로 진입한다. 빈 DB 면 여기서 early return 하여
|
||||
// 이후 플러그인 부팅/상태 조회가 table not found 로 부팅을 깨뜨리는 것을 막는다.
|
||||
if (! config('app.installer_completed') || InstallerContext::isSchemaMutatingCommand()) {
|
||||
try {
|
||||
if (! Schema::hasTable('plugins')) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\ApiDoc;
|
||||
|
||||
use App\Contracts\ApiDoc\ApiDocSampleSeeder;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\LanguagePack;
|
||||
use App\Models\Menu;
|
||||
use App\Models\NotificationDefinition;
|
||||
use App\Models\NotificationLog;
|
||||
use App\Models\NotificationTemplate;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* API 문서 실측용 완전 샘플 데이터 서비스
|
||||
*
|
||||
* 각 API 도메인의 대표 엔티티에 모든 필드가 채워진 완전한 샘플 레코드를
|
||||
* 멱등하게 생성하고, docgen 이 상세 GET 실측에 사용할 도메인별 대표 route key
|
||||
* 맵을 제공합니다. 개발 환경 전용 — 생성된 샘플은 개발 DB 에 남습니다.
|
||||
*/
|
||||
class ApiDocSampleService implements ApiDocSampleSeeder
|
||||
{
|
||||
/**
|
||||
* @var string 샘플 레코드 식별용 마커 (이메일/식별자 prefix)
|
||||
*/
|
||||
private const MARKER = 'apidoc-sample';
|
||||
|
||||
/**
|
||||
* 도메인별 완전 샘플을 멱등 생성하고 대표 route key 맵을 반환합니다.
|
||||
*
|
||||
* @return array<string, array{model: class-string, key: string, value: string}> 도메인 => 대표 레코드 정보
|
||||
*/
|
||||
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 채움)을 생성합니다.
|
||||
*
|
||||
* `scope=core` + `status=active` 조합은 그 locale 을 시스템 전역 지원 로케일로 승격시킨다
|
||||
* (LanguagePackRepository::getActiveCoreLocales → refreshSupportedLocales →
|
||||
* config('app.supported_locales'/'app.translatable_locales')).
|
||||
*
|
||||
* 문서용 샘플은 조회 예시를 만들기 위한 것이지 시스템 동작을 바꿀 자격이 없다. 샘플이 그
|
||||
* 경로를 타면 이후 시드/저장되는 모든 다국어 데이터에 실재하지 않는 로케일 키가 박히고,
|
||||
* 샘플을 비활성화한 뒤에는 그 키가 검증(TranslatableField)을 통과하지 못해 저장이 막힌다.
|
||||
*
|
||||
* 따라서 승격 경로를 벗어난 조합(module 스코프 + installed 상태)으로 생성하며, locale 도
|
||||
* 실재하는 번들 로케일만 사용한다.
|
||||
*
|
||||
* @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' => 'module',
|
||||
'target_identifier' => 'apidoc-sample-module',
|
||||
'locale' => 'ja',
|
||||
'locale_name' => 'Japanese',
|
||||
'locale_native_name' => '日本語',
|
||||
'text_direction' => 'ltr',
|
||||
'version' => '1.0.0',
|
||||
'latest_version' => '1.0.0',
|
||||
'license' => 'MIT',
|
||||
'description' => ['ko' => '문서 실측용 언어팩', 'en' => 'Sample language pack'],
|
||||
'status' => 'installed',
|
||||
'is_protected' => false,
|
||||
'manifest' => [
|
||||
'name' => ['ko' => 'API 문서 샘플 언어팩', 'en' => 'API Doc Sample Pack'],
|
||||
'version' => '1.0.0',
|
||||
'locale' => 'ja',
|
||||
],
|
||||
'source_type' => 'bundled',
|
||||
'installed_by' => $installer?->id,
|
||||
'installed_at' => now()->subDays(3),
|
||||
'activated_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\ApiDoc;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
|
||||
/**
|
||||
* API 엔드포인트 실측 프로브
|
||||
*
|
||||
* 임시 Sanctum 토큰을 발급해 실제 HTTP 요청으로 엔드포인트를 호출하고,
|
||||
* 실제 응답 JSON 에서 필드 스키마(키·타입·샘플값)를 관측합니다.
|
||||
* 쓰기 메서드는 기본적으로 실호출하지 않으며 GET/HEAD 만 read-only 로 실측합니다.
|
||||
*/
|
||||
class ApiEndpointProbe
|
||||
{
|
||||
/**
|
||||
* @var string 실측 대상 기준 URL
|
||||
*/
|
||||
private string $baseUrl;
|
||||
|
||||
/**
|
||||
* @var string|null 발급된 임시 토큰 평문
|
||||
*/
|
||||
private ?string $token = null;
|
||||
|
||||
/**
|
||||
* @var string 임시 토큰 식별용 이름
|
||||
*/
|
||||
private string $tokenName = 'api-docgen-probe';
|
||||
|
||||
/**
|
||||
* @var User|null 실측 인증 사용자 (in-process 쓰기 실측 시 로그인 대상)
|
||||
*/
|
||||
private ?User $user = null;
|
||||
|
||||
/**
|
||||
* @param string|null $baseUrl 기준 URL (null 이면 .env 의 APP_URL 직접 사용)
|
||||
*/
|
||||
public function __construct(?string $baseUrl = null)
|
||||
{
|
||||
// config('app.url') 은 테스트 환경에서 override 될 수 있으므로(test.example.com 등),
|
||||
// 실측은 .env 의 APP_URL 을 우선 신뢰한다. 명시 인자가 있으면 그것을 최우선한다.
|
||||
$resolved = $baseUrl
|
||||
?: (string) env('APP_URL')
|
||||
?: (string) config('app.url');
|
||||
|
||||
$this->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->user = $user;
|
||||
$this->cleanupTokens();
|
||||
$this->token = $user->createToken($this->tokenName)->plainTextToken;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 엔드포인트를 실호출하여 응답을 관측합니다.
|
||||
*
|
||||
* GET/HEAD 는 외부 HTTP 로 read-only 실측하고, 쓰기 메서드(POST/PUT/PATCH/DELETE)는
|
||||
* **DB 트랜잭션 안에서 in-process 로 dispatch 한 뒤 롤백**하여(영속 안 함) 응답 shape 만
|
||||
* 관측한다. 외부 HTTP 는 별도 프로세스라 롤백이 불가하므로 쓰기는 in-process 경로를 쓴다.
|
||||
*
|
||||
* @param string $method HTTP 메서드
|
||||
* @param string $uri 라우트 URI (path 파라미터 치환 완료된 실제 경로)
|
||||
* @param array<string, mixed> $body 쓰기 요청 바디 (쓰기 메서드에서만 사용)
|
||||
* @return array{ok: bool, status: int|null, body: array<string, mixed>|null, skipped_reason: string|null}
|
||||
*/
|
||||
public function probe(string $method, string $uri, array $body = []): array
|
||||
{
|
||||
$method = strtoupper($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'];
|
||||
}
|
||||
|
||||
if (in_array($method, ['GET', 'HEAD'], true)) {
|
||||
return $this->probeRead($method, $uri);
|
||||
}
|
||||
|
||||
return $this->probeWrite($method, $uri, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET/HEAD 를 외부 HTTP 로 read-only 실측합니다.
|
||||
*
|
||||
* @param string $method HTTP 메서드
|
||||
* @param string $uri 치환된 URI
|
||||
* @return array{ok: bool, status: int|null, body: array<string, mixed>|null, skipped_reason: string|null}
|
||||
*/
|
||||
private function probeRead(string $method, string $uri): array
|
||||
{
|
||||
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()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 쓰기 메서드를 DB 트랜잭션 안에서 in-process dispatch 후 롤백하여 실측합니다.
|
||||
*
|
||||
* 응답 shape 만 관측하고 부수효과(레코드 생성/수정/삭제)는 롤백으로 영속시키지 않는다.
|
||||
* 인증은 샘플 사용자로 로그인(guard)해 수행하며, DELETE 등 바디 없는 메서드도 처리한다.
|
||||
*
|
||||
* @param string $method HTTP 메서드
|
||||
* @param string $uri 치환된 URI
|
||||
* @param array<string, mixed> $body 요청 바디
|
||||
* @return array{ok: bool, status: int|null, body: array<string, mixed>|null, skipped_reason: string|null}
|
||||
*/
|
||||
private function probeWrite(string $method, string $uri, array $body): array
|
||||
{
|
||||
if (! $this->user) {
|
||||
return ['ok' => false, 'status' => null, 'body' => null, 'skipped_reason' => 'no-token'];
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// in-process dispatch 는 실제 Bearer 토큰 헤더로 인증한다(guard 종류 무관).
|
||||
// Auth::login() 은 API 기본 guard 가 RequestGuard(토큰 전용)라 사용할 수 없다.
|
||||
$request = Request::create($uri, $method, $body);
|
||||
$request->headers->set('Accept', 'application/json');
|
||||
$request->headers->set('Authorization', 'Bearer '.$this->token);
|
||||
|
||||
$response = app()->handle($request);
|
||||
$content = $response->getContent();
|
||||
$json = json_decode(is_string($content) ? $content : '', true);
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
return [
|
||||
'ok' => $status >= 200 && $status < 300 && is_array($json),
|
||||
'status' => $status,
|
||||
'body' => is_array($json) ? $json : null,
|
||||
'skipped_reason' => (is_array($json) && $status >= 200 && $status < 300) ? null : 'http-'.$status,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return ['ok' => false, 'status' => null, 'body' => null, 'skipped_reason' => 'write-probe-failed: '.$e->getMessage()];
|
||||
} finally {
|
||||
// 쓰기 부수효과를 영속시키지 않는다 (문서 생성 목적).
|
||||
DB::rollBack();
|
||||
// 다음 실측에 인증 컨텍스트가 새지 않도록 해석된 guard 를 초기화한다
|
||||
// (API guard 는 RequestGuard 라 logout() 이 없으므로 forgetGuards 사용).
|
||||
Auth::forgetGuards();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 발급한 임시 토큰을 정리합니다.
|
||||
*/
|
||||
public function cleanup(): void
|
||||
{
|
||||
$this->cleanupTokens();
|
||||
$this->token = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 실측용 토큰 레코드를 모두 삭제합니다.
|
||||
*/
|
||||
private function cleanupTokens(): void
|
||||
{
|
||||
PersonalAccessToken::query()
|
||||
->where('name', $this->tokenName)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\ApiDoc;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* API 라우트 인벤토리
|
||||
*
|
||||
* 등록된 라우트를 전수 수집하여 API 문서 생성에 필요한 메타데이터
|
||||
* (메서드·URI·라우트명·소유 확장·권한·컨트롤러 액션)로 정규화합니다.
|
||||
*/
|
||||
class ApiRouteInventory
|
||||
{
|
||||
/**
|
||||
* API 라우트를 전수 수집하여 정규화된 배열로 반환합니다.
|
||||
*
|
||||
* @param string|null $scope 범위 필터 (null=전체, 'core', 'module:{id}', 'plugin:{id}')
|
||||
* @return array<int, array<string, mixed>> 정규화된 라우트 메타데이터 목록
|
||||
*/
|
||||
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<int, array<string, mixed>> 정규화된 엔트리 목록
|
||||
*/
|
||||
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<int, string> 미들웨어 목록
|
||||
*/
|
||||
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<int, array<string, mixed>> 정규화된 라우트 메타데이터 목록
|
||||
*/
|
||||
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}.";
|
||||
|
||||
try {
|
||||
RouteFacade::prefix($urlPrefix)
|
||||
->name($namePrefix)
|
||||
->middleware('api')
|
||||
->group($apiRouteFile);
|
||||
RouteFacade::getRoutes()->refreshNameLookups();
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 이 확장 소유로 확정된 라우트를 전역 라우트 테이블에서 prefix(uri/name) 로 직접 골라낸다.
|
||||
// object-id 스냅샷 방식은 refreshNameLookups() 가 RouteCollection 을 재색인하며 객체를
|
||||
// 다시 만들어 dedup 이 어긋나므로(활성 확장 로드 순서·중복 로드에 취약) 쓰지 않는다.
|
||||
// 대상 확장은 uri 가 `api/{dir}/{id}/`, name 이 `api.{dir}.{id}.` 로 시작해 결정적으로 식별된다.
|
||||
$seen = [];
|
||||
$routes = [];
|
||||
|
||||
foreach (RouteFacade::getRoutes() as $route) {
|
||||
/** @var Route $route */
|
||||
$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) {
|
||||
// 파일이 이미 한 번 로드돼 있으면 같은 (method, uri) 가 중복될 수 있어 dedup 한다.
|
||||
$dedupKey = $entry['method'].' '.$entry['uri'];
|
||||
if (isset($seen[$dedupKey])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$dedupKey] = true;
|
||||
$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<int, string> 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<int, string> $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<int, string> path 파라미터명 목록
|
||||
*/
|
||||
private function pathParams(string $uri): array
|
||||
{
|
||||
preg_match_all('/\{([^}?]+)\??\}/', $uri, $m);
|
||||
|
||||
return $m[1] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* path 파라미터명 → 바인딩된 Eloquent 모델 클래스 맵을 추출합니다.
|
||||
*
|
||||
* @param Route $route 라우트 인스턴스
|
||||
* @return array<string, string> 파라미터명 => 모델 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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\ApiDoc;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 컬럼 주석 해석기
|
||||
*
|
||||
* 모델 테이블의 컬럼 주석(한국어 comment) 을 조회해 응답 필드 설명의 기본값으로
|
||||
* 제공합니다. G7 은 마이그레이션 한국어 comment 를 필수화하므로, 이 주석이 필드
|
||||
* 설명의 SSoT 가 됩니다. 정적 스키마 조회이므로 실측 없이도 동작합니다.
|
||||
*/
|
||||
class ColumnCommentResolver
|
||||
{
|
||||
/**
|
||||
* @var array<string, array<string, string>> 테이블별 컬럼 주석 캐시
|
||||
*/
|
||||
private array $cache = [];
|
||||
|
||||
/**
|
||||
* 모델 테이블의 컬럼명 => 주석 맵을 반환합니다.
|
||||
*
|
||||
* @param class-string|null $modelClass 모델 FQCN
|
||||
* @return array<string, string> 컬럼명 => 주석
|
||||
*/
|
||||
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<string, string> 컬럼명 => 주석
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\ApiDoc;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
use ReflectionNamedType;
|
||||
|
||||
/**
|
||||
* FormRequest 정적 분석기
|
||||
*
|
||||
* 컨트롤러 메서드 시그니처에서 FormRequest 를 찾아 rules() 를 리플렉션으로
|
||||
* 읽고, 요청 파라미터의 타입·필수 여부·허용값을 문서용 메타데이터로 변환합니다.
|
||||
*/
|
||||
class FormRequestIntrospector
|
||||
{
|
||||
/**
|
||||
* 컨트롤러 메서드의 첫 FormRequest 파라미터에서 rules 메타데이터를 추출합니다.
|
||||
*
|
||||
* @param string|null $controller 컨트롤러 FQCN
|
||||
* @param string|null $method 메서드명
|
||||
* @return array{request_class: string|null, params: array<int, array<string, mixed>>, hook_filters: array<int, string>}
|
||||
*/
|
||||
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<FormRequest>|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<FormRequest> $requestClass FormRequest FQCN
|
||||
* @return array<string, mixed> 검증 규칙 배열
|
||||
*/
|
||||
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<string, mixed> $rules 검증 규칙 배열
|
||||
* @return array<int, array<string, mixed>> 파라미터 메타데이터 목록
|
||||
*/
|
||||
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<int, string> $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<int, string> $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<FormRequest> $requestClass FormRequest FQCN
|
||||
* @return array<int, string> 훅 필터 이름 목록
|
||||
*/
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\ApiDoc;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* 요청 파라미터 설명기
|
||||
*
|
||||
* G7 전역에서 의미가 표준화된 공통 요청 파라미터(페이지네이션·정렬·검색·필터·
|
||||
* 소프트삭제 토글 등)와, 일관된 명명 규칙(*_id / *Id / is_* / *_date / sort_* 등)의
|
||||
* 설명을 코드에서 확인된 계약 그대로 서술합니다.
|
||||
*
|
||||
* ResourceFieldDescriber(응답 필드)의 요청 파라미터 대응물입니다. 도메인 특이
|
||||
* 파라미터(예: refund_priority, temp_key)는 여기서 커버하지 않고 사람 서술(TODO)로
|
||||
* 남깁니다 — 자동 채움은 "도메인 무관하게 의미가 고정된 파라미터"에만 한정합니다.
|
||||
*/
|
||||
class ParameterDescriber
|
||||
{
|
||||
/**
|
||||
* @var array<string, string> 정확 이름 => 설명 (위치 무관 공통 파라미터)
|
||||
*/
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\ApiDoc;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* 리소스 계약 필드 설명기
|
||||
*
|
||||
* 컬럼 주석이 없는 accessor/computed 필드(Resource 의 toArray() 가 파생하는 값)의
|
||||
* 설명을 제공합니다. G7 전역에서 의미가 표준화된 필드(BaseApiResource 의 is_owner /
|
||||
* abilities, Enum 파생 status_label / status_variant, User::isAdmin() 등)와, 일관된
|
||||
* 파생 규칙(_label / _variant / _flag / _name / is_* / *_at 접미·접두)을 코드에서
|
||||
* 확인된 의미 그대로 서술합니다.
|
||||
*
|
||||
* 우선순위: 이 계약 사전은 컬럼 주석보다 앞섭니다 — created_at 은 어느 테이블이든
|
||||
* "생성 일시" 이고, status_label 은 컬럼이 아니라 Enum label() 산물이기 때문입니다.
|
||||
*/
|
||||
class ResourceFieldDescriber
|
||||
{
|
||||
/**
|
||||
* @var array<string, string> 정확 필드명 => 설명 (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)',
|
||||
|
||||
// 감사/관계 공통 (도메인 무관하게 역할 고정 — 쓰기 응답에서 흔히 등장)
|
||||
'updated_by' => '최종 수정한 사용자 정보 (uuid/name — updated_by 관계 파생, 없으면 null)',
|
||||
'creator' => '생성자 정보 객체 (uuid/name/email — creator 관계 파생)',
|
||||
'user' => '대상 사용자 정보 객체 (uuid/name/email 등 — user 관계 파생)',
|
||||
'roles' => '보유 역할 목록 (각 원소 id/name/permissions — roles 관계 파생)',
|
||||
'permissions' => '권한 목록 (각 원소 identifier/name — permissions 관계 파생)',
|
||||
'avatar' => '아바타 이미지 URL (미등록 시 null)',
|
||||
|
||||
// 인증/토큰 (로그인/토큰 발급 응답)
|
||||
'token' => '발급된 API 접근 토큰 평문 (Bearer 토큰으로 사용, 발급 시 1회만 노출)',
|
||||
'token_type' => '토큰 타입 (일반적으로 Bearer)',
|
||||
'trigger_type' => '동작을 유발한 방식/주체 구분 값',
|
||||
|
||||
// 스케줄/실행 이력 (Schedule 실행 결과)
|
||||
'last_duration' => '마지막 실행 소요 시간 (초/밀리초 — 실행 이력 파생)',
|
||||
'duration' => '실행 소요 시간 (초/밀리초)',
|
||||
'exit_code' => '실행 종료 코드 (0=성공, 그 외=실패)',
|
||||
'output' => '실행 표준 출력 내용',
|
||||
'error_output' => '실행 표준 에러 출력 내용 (없으면 빈 문자열/null)',
|
||||
'memory_usage' => '실행 중 최대 메모리 사용량 (바이트)',
|
||||
|
||||
// 캐시/파일/집계 (일회성 동작 응답)
|
||||
'ttl' => '캐시 유효 시간 (Time To Live, 초)',
|
||||
'size_bytes' => '크기 (바이트)',
|
||||
'older_than_days' => '기준 경과 일수 (이 일수보다 오래된 대상 필터/집계)',
|
||||
'entries' => '항목 목록 (각 원소는 대상 도메인 레코드)',
|
||||
'changelog_entries' => '변경 이력 항목 목록 (버전별 변경 내용)',
|
||||
'templates' => '템플릿 목록 (각 원소 identifier/name 등 — 템플릿 관계 파생)',
|
||||
'spec' => '스펙 정의 객체 (편집기/컴포넌트 선언 스키마 등)',
|
||||
'source_meta' => '원천 메타데이터 객체 (출처·경로·해석 정보)',
|
||||
'validation_summary' => '검증 결과 요약 객체 (통과/실패 건수 등)',
|
||||
];
|
||||
|
||||
/**
|
||||
* 필드명에 대한 설명을 반환합니다. 없으면 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;
|
||||
}
|
||||
|
||||
// status 는 도메인마다 허용값 집합이 다르나(active/blocked, success/failed 등) 역할은
|
||||
// 공통이다. 문자열 상태값이면 일반 설명을 주고, 구체 허용값은 status_label/변형 필드와
|
||||
// 각 도메인 문서에서 보강한다. (동반 필드 status_label/status_variant 는 EXACT 로 설명됨)
|
||||
if ($field === 'status' && in_array($type, ['string', 'integer'], true)) {
|
||||
return '상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조)';
|
||||
}
|
||||
|
||||
if (isset(self::EXACT[$field])) {
|
||||
return self::EXACT[$field];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\ApiDoc;
|
||||
|
||||
/**
|
||||
* 응답 스키마 추론기
|
||||
*
|
||||
* 실측한 응답 JSON(ResponseHelper envelope)에서 data 내부의 필드 스키마를
|
||||
* 추론합니다. 각 필드의 타입과 샘플값을 문서용 메타데이터로 변환합니다.
|
||||
*/
|
||||
class ResponseSchemaInferrer
|
||||
{
|
||||
/**
|
||||
* envelope 응답 body 에서 data 필드 스키마를 추론합니다.
|
||||
*
|
||||
* @param array<string, mixed> $body 실측 응답 body (envelope)
|
||||
* @return array{envelope: array<int, string>, shape: string, fields: array<int, array<string, mixed>>, 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<string, mixed> $row 응답 데이터 한 행
|
||||
* @return array<int, array<string, mixed>> 필드 메타데이터 목록
|
||||
*/
|
||||
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<int, array<string, mixed>> $rows 응답 데이터 행 목록
|
||||
* @return array<int, array<string, mixed>> 필드 메타데이터 목록
|
||||
*/
|
||||
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<mixed> $arr 배열
|
||||
* @return bool 연관 배열 여부
|
||||
*/
|
||||
private function isAssoc(array $arr): bool
|
||||
{
|
||||
if ($arr === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_keys($arr) !== range(0, count($arr) - 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/**
|
||||
* 부팅 시점 "DB 스키마 준비성" 판정에 쓰이는 인스톨러 컨텍스트 헬퍼.
|
||||
*
|
||||
* `config('app.installer_completed')` fast-path 는 프로덕션에서 `Schema::hasTable()`
|
||||
* 호출을 건너뛰어 매 요청 오버헤드를 제거한다. 그러나 `INSTALLER_COMPLETED=true` 가
|
||||
* 기록된 `.env` 를 빈 DB 새 서버에 복사한 뒤 `php artisan migrate` 로 테이블을
|
||||
* 만들기 전에 앱이 부팅되면, fast-path 가 테이블 존재를 잘못 전제하여 뒤따르는
|
||||
* 쿼리가 "table not found" 로 부팅을 깨뜨린다.
|
||||
*
|
||||
* 이 헬퍼는 스키마를 파괴/재생성하는 마이그레이션 계열 콘솔 명령 판정을 단일
|
||||
* SSoT 로 캡슐화하여, 해당 컨텍스트에서 fast-path 를 무력화하는 가드에 쓰인다.
|
||||
*/
|
||||
class InstallerContext
|
||||
{
|
||||
/**
|
||||
* 스키마를 파괴/재생성하는 콘솔 명령(마이그레이션 계열)이 실행 중인지 판정합니다.
|
||||
*
|
||||
* 이 컨텍스트에서는 `installer_completed=true` 라도 테이블이 아직 없을 수 있으므로
|
||||
* fast-path(hasTable 스킵)를 신뢰해서는 안 됩니다. 웹 요청 등 콘솔이 아닌
|
||||
* 실행 경로에서는 항상 false 를 반환합니다.
|
||||
*
|
||||
* @return bool 마이그레이션 계열 명령 실행 중 여부
|
||||
*/
|
||||
public static function isSchemaMutatingCommand(): bool
|
||||
{
|
||||
if (! app()->runningInConsole()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array(
|
||||
$_SERVER['argv'][1] ?? null,
|
||||
['migrate', 'migrate:fresh', 'migrate:refresh', 'migrate:rollback', 'migrate:reset', 'db:wipe'],
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -231,7 +231,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'version' => env('APP_VERSION', '7.0.2'),
|
||||
'version' => env('APP_VERSION', '7.0.3'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -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<User>
|
||||
*/
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-2
@@ -9,7 +9,7 @@
|
||||
|
||||
| 카테고리 | 문서 수 | 링크 상태 |
|
||||
|----------|---------|----------|
|
||||
| [백엔드](backend/) | 32개 | 정상 |
|
||||
| [백엔드](backend/) | 33개 | 정상 |
|
||||
| [프론트엔드](frontend/) | 51개 | 정상 |
|
||||
| [확장 시스템](extension/) | 31개 | 정상 |
|
||||
| 공통 | 20개 | 정상 |
|
||||
@@ -19,6 +19,18 @@
|
||||
|
||||
---
|
||||
|
||||
## API 레퍼런스
|
||||
|
||||
G7 이 제공하는 REST API 의 엔드포인트별 요청 파라미터·응답 필드·요청/응답 예시입니다.
|
||||
템플릿의 `data_sources` 를 작성하거나 외부에서 G7 을 연동할 때 참고하세요.
|
||||
|
||||
| 문서 | 설명 |
|
||||
|------|------|
|
||||
| [backend/api/README.md](backend/api/README.md) | **API 레퍼런스 진입점** — 공통 규약(인증·응답 봉투·페이지네이션·에러) + 코어/확장 전체 목차 |
|
||||
| [backend/api-documentation.md](backend/api-documentation.md) | API 문서 작성·갱신 규정 (기여자용) |
|
||||
|
||||
---
|
||||
|
||||
## 작업 유형별 필수 문서
|
||||
|
||||
### 레이아웃 JSON 작성
|
||||
@@ -113,13 +125,14 @@
|
||||
<!-- AUTO-GENERATED-START: docs-readme-full-list -->
|
||||
## 카테고리별 전체 문서 목록
|
||||
|
||||
### 백엔드 (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 (실시간 이벤트) |
|
||||
|
||||
@@ -15,6 +15,16 @@
|
||||
|
||||
---
|
||||
|
||||
## API 레퍼런스
|
||||
|
||||
엔드포인트별 요청 파라미터·응답 필드·요청/응답 예시는 [api/README.md](api/README.md) 에 있습니다.
|
||||
공통 규약(Bearer 토큰 인증, 응답 봉투, 페이지네이션, 401/403/422/428 에러)도 그 문서 상단에 정리되어 있습니다.
|
||||
확장(모듈·플러그인)이 소유한 API 문서 목록도 같은 문서에서 찾을 수 있습니다.
|
||||
|
||||
문서 작성·갱신 규정은 [api-documentation.md](api-documentation.md) 를 참고하세요.
|
||||
|
||||
---
|
||||
|
||||
<!-- AUTO-GENERATED-START: backend-readme-docs -->
|
||||
## 문서 목록
|
||||
|
||||
@@ -23,6 +33,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) |
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
# 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. 추출 불가분(훅 주입 파라미터·동적 응답)은 <!-- TODO --> 마커 남기고 사람이 채움
|
||||
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`)으로 커맨드가 자동 분류한다.
|
||||
|
||||
### 문서 목차와 발견성 (README.md 규약)
|
||||
|
||||
각 대상(코어·확장)의 API 문서 디렉토리에는 `README.md` 목차가 있어야 한다. `api:docgen` 이 도메인 파일
|
||||
목록·엔드포인트 수를 담아 자동 생성한다(`@generated` 블록, 재생성 멱등, 블록 밖 사람 서술은 보존).
|
||||
|
||||
- 코어: `docs/backend/api/README.md`
|
||||
- 확장: `{modules|plugins}/_bundled/{id}/docs/api/README.md`
|
||||
|
||||
코어 README 는 프로젝트 최상위 `README.md` 의 "API 레퍼런스" 진입점이다. 따라서 확장 목차와 달리 세
|
||||
부분으로 구성된다. 이 문서(작성 규정)와 혼동되지 않도록 최상위 `README.md` 는 둘을 "API 레퍼런스" 와
|
||||
"API 문서 작성 규정" 으로 분리해 링크한다.
|
||||
|
||||
| 구성 | 위치 | 소유 |
|
||||
| --- | --- | --- |
|
||||
| 공통 규약 개요 (인증·응답 봉투·페이지네이션·에러) | 헤더 인용 블록 뒤 ~ 첫 `@generated` 앞 | 사람 (재생성 시 원문 보존) |
|
||||
| 코어 도메인 목차 | `@generated:start:api-readme-index` | `api:docgen` |
|
||||
| 확장 API 목차 | `@generated:start:api-readme-extensions` | `api:docgen` (코어 README 전용) |
|
||||
|
||||
개요를 첫 생성 블록 앞에 두는 이유는 목차 표보다 먼저 읽혀야 하기 때문이다. 확장 README 에는 확장 목차
|
||||
블록을 넣지 않는다. `--scope` 를 좁혀 실행해도 이번 회차에 갱신하지 않는 블록과 사람 서술은 원문 그대로
|
||||
보존된다.
|
||||
|
||||
확장 API 문서의 발견성은 이 목차를 통해 확보한다. `api:docgen` 과 코어 인덱스 생성기
|
||||
(`generate-docs-index.cjs`) 는 확장명을 하드코딩하지 않고
|
||||
`{modules,plugins}/_bundled/*/docs/api/README.md` 를 **패턴 스캔**해, 코어 README 의 "확장 API 레퍼런스"
|
||||
표와 AGENTS.md·docs-index 에 자동 편입한다(동적 로딩 원칙 — 코어는 규약과 스캔 패턴만, 확장
|
||||
이름은 파일 시스템에서 발견). 문서 수·엔드포인트 수는 각 README 의 집계 라인
|
||||
(`**문서 수**: N · **엔드포인트 수**: M`)에서 읽으므로, 이 라인 형식을 바꾸면 두 스캐너를 함께 갱신한다.
|
||||
|
||||
처음 진입하는 개발자/AI 의 도달 경로:
|
||||
|
||||
```text
|
||||
README.md "API 레퍼런스" 또는 AGENTS.md "API 레퍼런스 진입점" 표
|
||||
→ docs/backend/api/README.md (공통 규약 + 코어 목차 + 확장 목차)
|
||||
→ {도메인}.md 또는 {확장}/docs/api/README.md
|
||||
→ 엔드포인트별 파라미터·응답·예시
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 표준 문서 포맷
|
||||
|
||||
엔드포인트 1개당 아래 6개 구성(헤더 · 요청 파라미터 · 요청 예시 · 응답 필드 · 응답 예시 · 에러 응답)을
|
||||
따른다. `<!-- @generated:start -->` ~ `<!-- @generated:end -->` 사이는 `api:docgen` 이 재생성하는 추출
|
||||
블록이며, 그 바깥의 사람 서술(`**설명**`)은 재생성 시 보존된다.
|
||||
|
||||
에러 응답 표는 라우트 메타에서 대표 상태코드를 자동 추론한다: 인증 필수(`auth:sanctum`)→401,
|
||||
`admin`/`permission:` 요구→403, FormRequest 검증 규칙 존재→422, path 파라미터 존재→404.
|
||||
`optional.sanctum`(선택 인증)은 401 을 유발하지 않는다. 도메인 특이 에러(409·429 등)는 사람이 보강한다.
|
||||
|
||||
**요청 예시**(raw HTTP 요청)와 **응답 예시**(envelope 전문 JSON)는 실제 호출을 재현할 수 있도록 실측 기반으로
|
||||
방출된다(단계 6). 요청 예시는 curl 이 아니라 raw HTTP 요청(요청 라인 + 헤더)으로 표기해 응답 예시의
|
||||
`HTTP/1.1 {status}` 상태줄과 대칭을 이룬다. 세부 규칙:
|
||||
|
||||
- **요청 예시**: 요청 라인(`{METHOD} {path} HTTP/1.1`) + `Host:` + `Accept: application/json`, 인증 필요 시
|
||||
`Authorization: Bearer {YOUR_TOKEN}`(실측 토큰 평문 유출 방지 마스킹). `Host` 는 실측 기준 URL(로컬 개발
|
||||
호스트 등)을 노출하지 않고 공개 placeholder(`api.example.com`, RFC 2606 예약 도메인)로 마스킹한다.
|
||||
- **바디 메서드(POST/PUT/PATCH)**: `Content-Type: application/json` + 빈 줄 뒤 JSON 바디. 바디는 필수만이
|
||||
아니라 **전체 파라미터**를 담고 값은 이름·타입 기반 현실적 예시값으로 채운다(`"string"` placeholder 남발
|
||||
금지). 허용값 `in:` 열거가 있으면 그 첫 값을 채택한다.
|
||||
- **파일 업로드**(요청 파라미터 타입이 `image`/`file`): `application/json` 이 아니라
|
||||
`Content-Type: multipart/form-data; boundary=...` 로 표기하고 각 파일 파트를 `filename=`+`Content-Type`
|
||||
으로 나타낸다(JSON 으로는 파일 전송 불가).
|
||||
- **GET/DELETE**: query 파라미터는 URL 쿼리스트링(`?a=..&b=..`)으로 반영한다(바디 아님).
|
||||
- `optional.sanctum`(선택 인증) 엔드포인트는 Authorization 헤더에 "비회원은 생략 가능" 주석을 붙인다.
|
||||
- **응답 예시**: `HTTP/1.1 {status}` 상태줄 + 실측 응답 body 전문(`{success, data, message, error}` envelope).
|
||||
목록 응답의 `data.data[]` 는 대표 **2항목**으로 절단하고 나머지는 `... (총 N건 중 2건 표시)` 항목으로 대체한다.
|
||||
응답에 섞여 나온 **민감값(토큰·비밀번호·시크릿·API 키)은 `{MASKED}` 로 마스킹**해 방출한다.
|
||||
- **쓰기 메서드 실측**: GET/HEAD 는 외부 HTTP 로 read-only 실측하고, 쓰기(POST/PUT/PATCH/DELETE)는 **DB 트랜잭션
|
||||
안에서 in-process dispatch 후 롤백**하여 응답 shape 만 관측한다(부수효과 미영속). 단, 부수효과가 롤백으로
|
||||
되돌릴 수 없는 쓰기(확장 install/activate/update, 언어팩 설치, 코어 업데이트, 파일 업로드, 캐시/워밍업/생성
|
||||
등 파일시스템·프로세스·외부 네트워크 접촉)는 실측에서 제외(`side-effectful-write`)하고 요청 예시만 정적 방출한다.
|
||||
- **실측 제외**(부수효과 쓰기·바이너리·미치환 path 파라미터 등) 엔드포인트: 요청 예시는 파라미터 표 기반으로 정적
|
||||
조립(raw HTTP 요청 골격), 응답 예시는 `<!-- 실측 제외: {사유} — 응답 예시는 사람이 작성하세요. -->` 마커로 남긴다.
|
||||
- 두 예시 블록은 전량 `@generated` 블록 **내부**에 있다. 다만 이미 파라미터/응답 필드 표의 사람 서술이
|
||||
채워진 문서에 예시를 추가할 때는 전체 재생성(`api:docgen`)을 쓰지 않는다 — 전체 재생성은 표를 통째로
|
||||
다시 조립해 정적 추출로 재현 불가능한 도메인 서술 셀을 TODO 로 되돌린다. 대신 `api:docgen --examples-only`
|
||||
로 표·서술을 건드리지 않고 예시 2블록만 in-place 삽입/치환한다(측정은 재수행하되 표는 불가침). 백필
|
||||
커맨드(`backfill-params`/`backfill-fields`)도 예시 블록을 건드리지 않는다.
|
||||
- **`--examples-only` 모드**: 기존 문서의 각 엔드포인트 `@generated` 블록에 요청 예시(응답 필드 표 앞)와
|
||||
응답 예시(에러 응답 표 앞)만 삽입한다. 이미 예시가 있으면 그 블록만 새 실측으로 치환(멱등, 실측 상대시간
|
||||
필드는 재측정으로 값이 달라질 수 있음). 표·필드 설명·엔드포인트 서술은 전량 보존된다. 최초 스캐폴딩이
|
||||
없는 신규 문서는 먼저 `api:docgen` 으로 생성한 뒤 서술을 채우고, 이후 예시는 `--examples-only` 로 유지한다.
|
||||
|
||||
```markdown
|
||||
### GET /api/admin/users
|
||||
<!-- @generated:start:api.admin.users.index -->
|
||||
- **라우트명**: `api.admin.users.index`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@index`
|
||||
- **인증/권한**: `auth:sanctum` + `admin` + `permission:admin,core.users.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
|------|------|------|------|--------|------|
|
||||
| keyword | query | string | 아니오 | — | <!-- TODO: 용도 --> |
|
||||
| status | query | string | 아니오 | `active`, `dormant`, `withdrawn` | <!-- TODO: 용도 --> |
|
||||
|
||||
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있음 (`core.user.search_validation_rules`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/users HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
|------|------|-------------|-----------|
|
||||
| id | integer | `1` | <!-- TODO: 설명 --> |
|
||||
| uuid | string | `a231747f-...` | <!-- TODO: 설명 --> |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"data": [
|
||||
{ "id": 1, "uuid": "a231747f-..." },
|
||||
{ "id": 2, "uuid": "b0f2..." },
|
||||
"... (총 42건 중 2건 표시)"
|
||||
],
|
||||
"pagination": { "current_page": 1, "total": 42 }
|
||||
},
|
||||
"message": null,
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`admin,core.users.read`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** <!-- 사람이 작성: 이 엔드포인트의 용도, 주의사항, 예시 시나리오 -->
|
||||
```
|
||||
|
||||
### 응답 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
|
||||
|
||||
# 이미 서술이 채워진 문서에 요청/응답 예시 블록만 in-place 삽입 (표·서술 불가침, 재생성 금지)
|
||||
php artisan api:docgen --scope=module:sirsoft-board --seed --base-url=https://example.dev --examples-only
|
||||
|
||||
# 생성될 대상만 미리보기
|
||||
php artisan api:docgen --scope=core --dry-run
|
||||
```
|
||||
|
||||
동작 (실측 기반):
|
||||
|
||||
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` 로 활성 디렉토리에
|
||||
반영해야 오토로드된다.
|
||||
|
||||
#### 샘플은 시스템 동작을 바꾸지 않는다
|
||||
|
||||
`--seed` 로 만든 샘플 레코드는 개발 DB 에 영구 잔존한다. 조회 예시를 만들기 위한 데이터일 뿐이므로,
|
||||
전역 설정을 읽는 쿼리에 걸리는 조합으로 생성해서는 안 된다.
|
||||
|
||||
| ❌ 금지 | ✅ 올바른 사용 |
|
||||
| --- | --- |
|
||||
| `LanguagePack::create(['scope' => 'core', 'status' => 'active', ...])` | `['scope' => 'module', 'target_identifier' => '...', 'status' => 'installed']` |
|
||||
| 실재하지 않는 로케일/통화/국가 코드를 샘플 값으로 사용 | 실재하는 번들 값만 사용 (`ja` 등) |
|
||||
|
||||
`scope=core` + `status=active` 조합은 `LanguagePackRepository::getActiveCoreLocales()` 의 승격 쿼리와
|
||||
일치한다. 걸리면 그 locale 이 `config('app.supported_locales')` / `config('app.translatable_locales')` 로
|
||||
올라가 시스템 전역이 바뀐다. 그 아래에서 시드·저장되는 모든 다국어 데이터에 실재하지 않는 로케일 키가
|
||||
박히고, 샘플을 비활성화한 뒤에는 그 키가 `TranslatableField` 검증을 통과하지 못해 해당 엔티티의 저장이
|
||||
막힌다. 오염 시점과 증상 발현 시점이 떨어져 있어 원인 추적이 어렵다.
|
||||
|
||||
같은 원리로 `is_default` / `status=active` 같은 승격·활성 플래그를 샘플에 붙이지 않는다. 샘플 레코드를
|
||||
만들기 전에 "이 조합이 전역 설정을 읽는 쿼리에 걸리는가" 를 확인한다.
|
||||
|
||||
자동 차단: audit 룰 `apidoc-sample-no-global-active-language-pack` (severity: error).
|
||||
|
||||
### 설명 백필 커맨드 (`api:docgen-backfill-*`)
|
||||
|
||||
파라미터/응답 필드 표의 `<!-- TODO: 설명 -->` 셀 중 **도메인 무관 공통 필드**(페이지네이션·정렬·검색·
|
||||
식별자·기간·토글·공통 파생 필드 등)는 전체 재생성 없이 in-place 로 자동 서술한다. 전체 재생성
|
||||
(`api:docgen`)은 `@generated` 블록 표를 통째로 재조립해 실측 예시값·사람 서술 셀을 되돌리므로, TODO
|
||||
셀만 치환하는 별도 백필 커맨드를 쓴다(멱등 — 채워진 셀·실측 예시값 불가침).
|
||||
|
||||
```bash
|
||||
# 요청 파라미터 표 TODO 셀 백필 (SSoT: App\Support\ApiDoc\ParameterDescriber)
|
||||
php artisan api:docgen-backfill-params
|
||||
|
||||
# 응답 필드 표 TODO 셀 백필 (SSoT: App\Support\ApiDoc\ResourceFieldDescriber)
|
||||
php artisan api:docgen-backfill-fields
|
||||
```
|
||||
|
||||
- 도메인 종속 필드(`status`/`type`/`code`/`category`·훅 주입·조건부 필드)는 값 의미가 도메인마다 달라
|
||||
자동 채우지 않고 TODO 로 남겨 사람이 컨트롤러/FormRequest/Resource 근거로 서술한다.
|
||||
- 새 공통 필드 규칙을 Describer 사전에 추가한 뒤 백필을 재실행하면 그 필드가 전 문서에서 자동 채워진다.
|
||||
|
||||
---
|
||||
|
||||
## 문서 갱신 의무
|
||||
|
||||
컨트롤러/라우트/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/)에 문서화되었는가?
|
||||
□ 요청 파라미터 표에 위치/타입/필수/허용값/용도가 모두 기재되었는가?
|
||||
□ 요청 예시(raw HTTP 요청) 블록이 방출되었는가? (curl 금지, 인증 필요 시 Bearer {YOUR_TOKEN} 마스킹)
|
||||
□ 응답 필드 표가 envelope 의 data 내부 기준으로 작성되었는가?
|
||||
□ 응답 예시(envelope 전문 JSON) 블록이 방출되었는가? (목록은 2항목 절단)
|
||||
□ 훅 주입 파라미터가 있으면 주석 + 사람 보강이 되었는가?
|
||||
□ 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 → 파라미터 허용값 유추 근거
|
||||
@@ -0,0 +1,172 @@
|
||||
# API 레퍼런스 문서 목차
|
||||
|
||||
> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반).
|
||||
> 아래 표는 자동 생성됩니다. 각 문서를 열면 엔드포인트별 파라미터·응답·예시를 볼 수 있습니다.
|
||||
|
||||
G7 의 REST API 레퍼런스입니다. 도메인별 문서에는 엔드포인트마다 메서드·URI·인증/권한, 요청 파라미터 표,
|
||||
응답 필드 표, 실제 호출로 관측한 요청·응답 예시가 실려 있습니다. 아래 공통 규약은 모든 엔드포인트에
|
||||
동일하게 적용되므로 개별 문서에서 반복하지 않습니다.
|
||||
|
||||
문서 작성·갱신 규정은 [api-documentation.md](../api-documentation.md) 를 참고하세요.
|
||||
|
||||
## 공통 규약
|
||||
|
||||
### 인증
|
||||
|
||||
Laravel Sanctum 의 **Bearer 토큰 전용**입니다(세션 쿠키 인증 미사용).
|
||||
`POST /api/auth/login` 또는 `POST /api/auth/admin/login` 으로 토큰을 발급받아 모든 후속 요청에 실어 보냅니다.
|
||||
|
||||
```http
|
||||
GET /api/me HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
각 문서의 **인증/권한** 줄은 다음 네 가지 중 하나입니다.
|
||||
|
||||
| 표기 | 의미 |
|
||||
| --- | --- |
|
||||
| `공개 (인증 불필요)` | 토큰 없이 호출 가능 |
|
||||
| `optional.sanctum` | 토큰이 있으면 회원, 없으면 비회원으로 처리 (둘 다 접근 가능) |
|
||||
| `auth:sanctum` | 유효한 토큰 필수 (없거나 만료 시 401) |
|
||||
| `auth:sanctum + admin + permission:{키}` | 토큰 + 관리자 + 해당 권한 필요 (권한 부족 시 403) |
|
||||
|
||||
### 로케일
|
||||
|
||||
응답 메시지의 언어는 ① 로그인 사용자의 `users.language` → ② `Accept-Language` 헤더 →
|
||||
③ 시스템 기본값(`config('app.locale')`) 순으로 결정됩니다. 지원 언어는 `config('app.supported_locales')` 를 따릅니다.
|
||||
|
||||
### 응답 봉투
|
||||
|
||||
성공·실패 모두 동일한 최상위 구조를 씁니다. 실제 페이로드는 항상 `data` 안에 들어갑니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "요청이 성공했습니다.",
|
||||
"data": { }
|
||||
}
|
||||
```
|
||||
|
||||
실패 시 `success` 는 `false` 이고, 검증 오류는 `errors` 에 필드별 메시지 배열로 담깁니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "입력값이 올바르지 않습니다.",
|
||||
"errors": {
|
||||
"email": ["이메일 형식이 올바르지 않습니다."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 페이지네이션
|
||||
|
||||
목록 엔드포인트는 `data.data[]` 에 항목 배열을, `data.pagination` 에 페이지 정보를 담습니다.
|
||||
요청은 `page` 와 `per_page` 쿼리 파라미터로 제어합니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 4,
|
||||
"per_page": 25,
|
||||
"total": 87,
|
||||
"from": 1,
|
||||
"to": 25,
|
||||
"has_more_pages": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
일부 목록은 `data.abilities` 에 컬렉션 레벨 권한(`can_create`, `can_delete` 등)을 함께 반환합니다.
|
||||
화면의 버튼 노출 여부를 이 값으로 판정하세요.
|
||||
|
||||
### 공통 에러 상태코드
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 엔드포인트가 요구하는 권한이 없는 경우 |
|
||||
| 404 | Not Found | 대상 리소스가 없거나 접근 범위를 벗어난 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`errors` 에 필드별 메시지) |
|
||||
| 428 | Precondition Required | 본인인증(IDV)이 선행되어야 하는 경우 |
|
||||
|
||||
428 응답은 `error_code: "identity_verification_required"` 와 함께 `verification` 객체를 반환합니다.
|
||||
클라이언트는 이 값으로 본인인증 화면을 띄운 뒤 원래 요청을 재시도합니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error_code": "identity_verification_required",
|
||||
"message": "본인인증이 필요합니다.",
|
||||
"verification": { }
|
||||
}
|
||||
```
|
||||
|
||||
## 코어 API 레퍼런스
|
||||
|
||||
<!-- @generated:start:api-readme-index -->
|
||||
- **문서 수**: 35 · **엔드포인트 수**: 291
|
||||
|
||||
| 문서 | 도메인 | 엔드포인트 |
|
||||
| --- | --- | --- |
|
||||
| [activity-logs.md](activity-logs.md) | `activity-logs` | 3 |
|
||||
| [attachment.md](attachment.md) | `attachment` | 1 |
|
||||
| [attachments.md](attachments.md) | `attachments` | 4 |
|
||||
| [auth.md](auth.md) | `auth` | 15 |
|
||||
| [avatar.md](avatar.md) | `avatar` | 2 |
|
||||
| [broadcasting.md](broadcasting.md) | `broadcasting` | 1 |
|
||||
| [changelog.md](changelog.md) | `changelog` | 1 |
|
||||
| [core-update.md](core-update.md) | `core-update` | 2 |
|
||||
| [dashboard.md](dashboard.md) | `dashboard` | 5 |
|
||||
| [extensions.md](extensions.md) | `extensions` | 3 |
|
||||
| [identity.md](identity.md) | `identity` | 27 |
|
||||
| [language-packs.md](language-packs.md) | `language-packs` | 15 |
|
||||
| [layouts.md](layouts.md) | `layouts` | 2 |
|
||||
| [license.md](license.md) | `license` | 1 |
|
||||
| [locales.md](locales.md) | `locales` | 1 |
|
||||
| [me.md](me.md) | `me` | 3 |
|
||||
| [menus.md](menus.md) | `menus` | 10 |
|
||||
| [modules.md](modules.md) | `modules` | 25 |
|
||||
| [notification-channels.md](notification-channels.md) | `notification-channels` | 1 |
|
||||
| [notification-definitions.md](notification-definitions.md) | `notification-definitions` | 5 |
|
||||
| [notification-logs.md](notification-logs.md) | `notification-logs` | 3 |
|
||||
| [notification-templates.md](notification-templates.md) | `notification-templates` | 4 |
|
||||
| [notifications.md](notifications.md) | `notifications` | 14 |
|
||||
| [password.md](password.md) | `password` | 1 |
|
||||
| [permissions.md](permissions.md) | `permissions` | 1 |
|
||||
| [plugins.md](plugins.md) | `plugins` | 27 |
|
||||
| [profile.md](profile.md) | `profile` | 4 |
|
||||
| [roles.md](roles.md) | `roles` | 7 |
|
||||
| [schedules.md](schedules.md) | `schedules` | 12 |
|
||||
| [search.md](search.md) | `search` | 1 |
|
||||
| [seo.md](seo.md) | `seo` | 5 |
|
||||
| [settings.md](settings.md) | `settings` | 15 |
|
||||
| [templates.md](templates.md) | `templates` | 57 |
|
||||
| [users.md](users.md) | `users` | 12 |
|
||||
| [verify-password.md](verify-password.md) | `verify-password` | 1 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
## 확장 API 레퍼런스
|
||||
|
||||
> 각 확장이 자신의 API 문서를 소유합니다. 아래 표는 자동 생성됩니다.
|
||||
|
||||
<!-- @generated:start:api-readme-extensions -->
|
||||
- **확장 수**: 9 · **엔드포인트 수**: 372
|
||||
|
||||
| 확장 | 유형 | API 문서 목차 | 문서/엔드포인트 |
|
||||
| --- | --- | --- | --- |
|
||||
| `gnuboard7-hello_module` | 모듈 | [docs/api/](../../../modules/_bundled/gnuboard7-hello_module/docs/api/README.md) | 1 / 2 |
|
||||
| `sirsoft-board` | 모듈 | [docs/api/](../../../modules/_bundled/sirsoft-board/docs/api/README.md) | 10 / 80 |
|
||||
| `sirsoft-ecommerce` | 모듈 | [docs/api/](../../../modules/_bundled/sirsoft-ecommerce/docs/api/README.md) | 33 / 231 |
|
||||
| `sirsoft-page` | 모듈 | [docs/api/](../../../modules/_bundled/sirsoft-page/docs/api/README.md) | 2 / 17 |
|
||||
| `sirsoft-ckeditor5` | 플러그인 | [docs/api/](../../../plugins/_bundled/sirsoft-ckeditor5/docs/api/README.md) | 2 / 2 |
|
||||
| `sirsoft-gdpr` | 플러그인 | [docs/api/](../../../plugins/_bundled/sirsoft-gdpr/docs/api/README.md) | 4 / 15 |
|
||||
| `sirsoft-marketing` | 플러그인 | [docs/api/](../../../plugins/_bundled/sirsoft-marketing/docs/api/README.md) | 2 / 2 |
|
||||
| `sirsoft-pay_kginicis` | 플러그인 | [docs/api/](../../../plugins/_bundled/sirsoft-pay_kginicis/docs/api/README.md) | 5 / 22 |
|
||||
| `sirsoft-verification_kginicis` | 플러그인 | [docs/api/](../../../plugins/_bundled/sirsoft-verification_kginicis/docs/api/README.md) | 1 / 1 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
@@ -0,0 +1,291 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.admin.activity-logs.index -->
|
||||
- **라우트명**: `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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/activity-logs?log_type=%EC%98%88%EC%8B%9C%EA%B0%92&action=%EC%98%88%EC%8B%9C%EA%B0%92&user_id=1&loggable_type=%EC%98%88%EC%8B%9C%EA%B0%92&search=%EC%98%88%EC%8B%9C%EA%B0%92&search_type=%EC%98%88%EC%8B%9C%EA%B0%92&created_by=%EC%98%88%EC%8B%9C%EA%B0%92&date_from=2026-01-01&date_to=2026-01-01&per_page=1&sort_by=%EC%98%88%EC%8B%9C%EA%B0%92&sort_order=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "활동 로그 정보를 성공적으로 가져왔습니다.",
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"number": 22,
|
||||
"id": 22,
|
||||
"log_type": "admin",
|
||||
"log_type_label": "관리자",
|
||||
"loggable_type": null,
|
||||
"loggable_type_display": null,
|
||||
"loggable_id": null,
|
||||
"action": "user.index",
|
||||
"action_label": "목록 조회",
|
||||
"localized_description": "사용자 목록 조회",
|
||||
"description_key": "activity_log.description.user_index",
|
||||
"properties": {
|
||||
"result_count": 2
|
||||
},
|
||||
"changes": null,
|
||||
"bulk_changes": null,
|
||||
"has_changes": false,
|
||||
"actor_name": "API 문서 샘플 사용자",
|
||||
"user": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 사용자",
|
||||
"email": "apidoc-sample-user@example.com"
|
||||
},
|
||||
"ip_address": "127.0.0.1",
|
||||
"created_at": "2026-07-08 12:12:12",
|
||||
"is_owner": true,
|
||||
"abilities": {
|
||||
"can_read": true,
|
||||
"can_delete": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"number": 21,
|
||||
"id": 21,
|
||||
"log_type": "admin",
|
||||
"log_type_label": "관리자",
|
||||
"loggable_type": null,
|
||||
"loggable_type_display": null,
|
||||
"loggable_id": null,
|
||||
"action": "user.index",
|
||||
"action_label": "목록 조회",
|
||||
"localized_description": "사용자 목록 조회",
|
||||
"description_key": "activity_log.description.user_index",
|
||||
"properties": {
|
||||
"result_count": 2
|
||||
},
|
||||
"changes": null,
|
||||
"bulk_changes": null,
|
||||
"has_changes": false,
|
||||
"actor_name": "API 문서 샘플 사용자",
|
||||
"user": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 사용자",
|
||||
"email": "apidoc-sample-user@example.com"
|
||||
},
|
||||
"ip_address": "127.0.0.1",
|
||||
"created_at": "2026-07-08 12:08:26",
|
||||
"is_owner": true,
|
||||
"abilities": {
|
||||
"can_read": true,
|
||||
"can_delete": true
|
||||
}
|
||||
},
|
||||
"... (총 22건 중 2건 표시)"
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"per_page": 25,
|
||||
"total": 22,
|
||||
"from": 1,
|
||||
"to": 22,
|
||||
"has_more_pages": false
|
||||
},
|
||||
"abilities": {
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.activities.read`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 시스템 활동 로그를 페이지네이션 목록으로 조회합니다. `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
|
||||
<!-- @generated:start:api.admin.activity-logs.bulk-destroy -->
|
||||
- **라우트명**: `api.admin.activity-logs.bulk-destroy`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\ActivityLogController@bulkDestroy`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.activities.delete`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/activity-logs/bulk-delete HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ids": [
|
||||
"예시값"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.activities.delete`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 지정한 활동 로그들을 일괄 삭제합니다. `ids` 배열에 삭제할 로그 ID를 담아 요청하며, 서비스가 각 항목을 삭제하고 실제 삭제된 건수(`deleted_count`)를 반환합니다. `core.activities.delete` 권한이 필요합니다. 로그 목록에서 여러 항목을 선택해 한 번에 정리하는 시나리오에 사용합니다.
|
||||
|
||||
|
||||
### DELETE /api/admin/activity-logs/{activityLog}
|
||||
<!-- @generated:start:api.admin.activity-logs.destroy -->
|
||||
- **라우트명**: `api.admin.activity-logs.destroy`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\ActivityLogController@destroy`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.activities.delete`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| activityLog | path | string | 예 | — | 대상 activity log의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/admin/activity-logs/1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
|
||||
|
||||
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "활동 로그가 삭제되었습니다.",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.activities.delete`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 단일 활동 로그를 삭제합니다. 경로의 `{activityLog}`는 라우트 모델 바인딩으로 로그 ID를 받아 해당 레코드를 삭제합니다. `core.activities.delete` 권한이 필요하며, 삭제 실패 시 오류가 로그로 기록되고 500이 반환됩니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# 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}
|
||||
<!-- @generated:start:api.attachment.download -->
|
||||
- **라우트명**: `api.attachment.download`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicAttachmentController@download`
|
||||
- **인증/권한**: 공개 (인증 불필요)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| hash | path | string | 예 | — | 대상 리소스의 해시 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/attachment/{hash} HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 해시(12자)로 식별되는 첨부파일을 다운로드합니다. 이미지 파일은 캐싱 헤더와 함께 인라인으로 표시하고 그 외 파일은 다운로드 방식으로 제공합니다. 인증이 필요 없는 공개 라우트이지만 접근 권한은 AttachmentService가 로그인/비로그인 사용자 모두를 대상으로 하이브리드 방식으로 검사하며, 파일이 없으면 404, 권한이 없으면 403을 반환합니다. 게시글 첨부·상품 이미지 등 공개 리소스를 URL로 직접 내려받는 시나리오에 사용합니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.admin.attachments.upload -->
|
||||
- **라우트명**: `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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/attachments HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: multipart/form-data; boundary=----G7ExampleBoundary
|
||||
|
||||
------G7ExampleBoundary
|
||||
Content-Disposition: form-data; name="file"; filename="example.pdf"
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
(바이너리 파일 내용)
|
||||
------G7ExampleBoundary
|
||||
Content-Disposition: form-data; name="attachmentable_type"
|
||||
|
||||
예시값
|
||||
------G7ExampleBoundary
|
||||
Content-Disposition: form-data; name="attachmentable_id"
|
||||
|
||||
1
|
||||
------G7ExampleBoundary
|
||||
Content-Disposition: form-data; name="collection"
|
||||
|
||||
예시값
|
||||
------G7ExampleBoundary
|
||||
Content-Disposition: form-data; name="source_type"
|
||||
|
||||
예시값
|
||||
------G7ExampleBoundary
|
||||
Content-Disposition: form-data; name="source_identifier"
|
||||
|
||||
example-key
|
||||
------G7ExampleBoundary--
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.attachments.create`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 단일 파일을 업로드해 첨부파일(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
|
||||
<!-- @generated:start:api.admin.attachments.upload_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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/attachments/batch HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"files": [
|
||||
"예시값"
|
||||
],
|
||||
"attachmentable_type": "예시값",
|
||||
"attachmentable_id": 1,
|
||||
"collection": "예시값",
|
||||
"source_type": "예시값",
|
||||
"source_identifier": "example-key"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.attachments.create`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 여러 파일을 한 번의 요청으로 일괄 업로드합니다. `files` 배열의 각 파일이 개별 첨부파일 레코드로 등록되며, `attachmentable_type`/`attachmentable_id`/`collection` 등의 옵션은 배치 전체에 공통 적용됩니다. `core.attachments.create` 권한이 필요하고, 성공 시 201과 함께 생성된 첨부파일 리소스 컬렉션을 반환합니다. 갤러리·다중 이미지 첨부처럼 한 대상에 여러 파일을 붙이는 시나리오에 사용합니다.
|
||||
|
||||
|
||||
### PATCH /api/admin/attachments/reorder
|
||||
<!-- @generated:start: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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PATCH /api/admin/attachments/reorder HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"order": [
|
||||
"예시값"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.attachments.update`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 첨부파일의 표시 순서를 재정렬합니다. `order` 배열에 담긴 순서대로 각 첨부파일의 정렬 값이 갱신됩니다. `core.attachments.update` 권한이 필요합니다. 갤러리에서 드래그 앤 드롭으로 이미지 순서를 바꾸는 등 이미 등록된 첨부파일의 나열 순서만 변경할 때 사용합니다.
|
||||
|
||||
|
||||
### DELETE /api/admin/attachments/{attachment}
|
||||
<!-- @generated:start:api.admin.attachments.destroy -->
|
||||
- **라우트명**: `api.admin.attachments.destroy`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\AttachmentController@destroy`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.attachments.delete`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| attachment | path | string | 예 | — | 대상 attachment의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/admin/attachments/{attachment} HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.attachments.delete`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 지정한 첨부파일을 삭제합니다. 경로의 `{attachment}`는 라우트 모델 바인딩으로 첨부파일 ID를 받으며, 서비스가 DB 레코드와 실제 저장 파일을 함께 제거합니다. `core.attachments.delete` 권한이 필요합니다. 존재하지 않는 ID면 404가 반환됩니다.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.me.avatar.delete -->
|
||||
- **라우트명**: `api.me.avatar.delete`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@deleteAvatar`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/me/avatar HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 현재 인증 사용자의 아바타를 삭제합니다. 사용자에 연결된 아바타 첨부파일(Attachment) 레코드와 실제 파일을 함께 제거하며, 삭제 활동이 로그로 기록됩니다. 아바타가 없으면 404(`user.avatar_not_found`)를 반환합니다. `auth:sanctum` 인증만 필요하고 별도 권한은 없으며, 사용자가 자신의 프로필 사진을 기본값으로 되돌리는 시나리오에 사용합니다.
|
||||
|
||||
|
||||
### POST /api/me/avatar
|
||||
<!-- @generated:start:api.me.avatar.upload -->
|
||||
- **라우트명**: `api.me.avatar.upload`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@uploadAvatar`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| avatar | body | image | 예 | max 2048 | 아바타 이미지 |
|
||||
|
||||
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.upload_avatar_rules`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/me/avatar HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: multipart/form-data; boundary=----G7ExampleBoundary
|
||||
|
||||
------G7ExampleBoundary
|
||||
Content-Disposition: form-data; name="avatar"; filename="example.png"
|
||||
Content-Type: image/png
|
||||
|
||||
(바이너리 파일 내용)
|
||||
------G7ExampleBoundary--
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 현재 인증 사용자의 아바타 이미지를 업로드합니다. 기존 아바타가 있으면 먼저 삭제한 뒤 새 이미지를 `avatar` 컬렉션의 다형성 첨부파일로 등록하고, 업로드 활동을 로그로 기록합니다. `auth:sanctum` 인증만 필요하고 별도 권한은 없으며, 이미지는 최대 2048KB로 제한됩니다. 확장은 `core.user.upload_avatar_rules` 훅으로 검증 규칙을 추가할 수 있습니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.broadcasting.auth -->
|
||||
- **라우트명**: `api.broadcasting.auth`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/broadcasting/auth HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** WebSocket 프라이빗/프레즌스 채널 구독 시 Laravel Broadcast 채널 인증을 수행하는 엔드포인트입니다. `auth:sanctum` 토큰으로 인증하며, 웹소켓 사용이 OFF(`broadcasting.default === 'null'`)이면 채널 인증을 거부해 403을 반환합니다(reverb.key 무력화를 우회한 직접 연결 시도까지 차단). 컨트롤러 없이 라우트 클로저가 토글 가드를 적용한 뒤 `Broadcast::auth`에 위임하며, 실시간 이벤트 구독을 위해 클라이언트 브로드캐스팅 라이브러리가 자동 호출합니다.
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,400 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.admin.dashboard.activities -->
|
||||
- **라우트명**: `api.admin.dashboard.activities`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\DashboardController@activities`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.dashboard.activities`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/dashboard/activities HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`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) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "최근 활동을 성공적으로 조회했습니다.",
|
||||
"data": [
|
||||
{
|
||||
"type": "admin",
|
||||
"icon": "circle-info",
|
||||
"icon_color": "blue",
|
||||
"title": "사용자 목록 조회",
|
||||
"description": "API 문서 샘플 사용자",
|
||||
"time": "2분 전",
|
||||
"timestamp": "2026-07-08T12:12:12+09:00"
|
||||
},
|
||||
{
|
||||
"type": "admin",
|
||||
"icon": "circle-info",
|
||||
"icon_color": "blue",
|
||||
"title": "사용자 목록 조회",
|
||||
"description": "API 문서 샘플 사용자",
|
||||
"time": "6분 전",
|
||||
"timestamp": "2026-07-08T12:08:26+09:00"
|
||||
},
|
||||
{
|
||||
"type": "admin",
|
||||
"icon": "circle-info",
|
||||
"icon_color": "blue",
|
||||
"title": "사용자 목록 조회",
|
||||
"description": "API 문서 샘플 사용자",
|
||||
"time": "9분 전",
|
||||
"timestamp": "2026-07-08T12:04:38+09:00"
|
||||
},
|
||||
{
|
||||
"type": "admin",
|
||||
"icon": "circle-info",
|
||||
"icon_color": "blue",
|
||||
"title": "사용자 목록 조회",
|
||||
"description": "API 문서 샘플 사용자",
|
||||
"time": "43분 전",
|
||||
"timestamp": "2026-07-08T11:31:03+09:00"
|
||||
},
|
||||
{
|
||||
"type": "admin",
|
||||
"icon": "circle-info",
|
||||
"icon_color": "blue",
|
||||
"title": "사용자 목록 조회",
|
||||
"description": "API 문서 샘플 사용자",
|
||||
"time": "45분 전",
|
||||
"timestamp": "2026-07-08T11:29:30+09:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.dashboard.activities`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 관리자 대시보드에 표시할 최근 활동 내역(사용자 등록, 모듈 활성화 등)을 조회합니다. 인증(`auth:sanctum`)과 `core.dashboard.activities` 권한이 필요합니다. 각 항목은 유형·아이콘·제목·설명과 상대 시간(`time`)·절대 시각(`timestamp`)을 포함하며, 대시보드 최근 활동 카드를 렌더링할 때 사용합니다.
|
||||
|
||||
|
||||
### GET /api/admin/dashboard/alerts
|
||||
<!-- @generated:start:api.admin.dashboard.alerts -->
|
||||
- **라우트명**: `api.admin.dashboard.alerts`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\DashboardController@alerts`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.dashboard.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/dashboard/alerts HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
|
||||
|
||||
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "시스템 알림을 성공적으로 조회했습니다.",
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.dashboard.read`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 시스템 업데이트·경고 등 관리자에게 알릴 시스템 알림 목록을 조회합니다. 인증(`auth:sanctum`)과 `core.dashboard.read` 권한이 필요합니다. 알릴 항목이 없으면 빈 목록을 반환하며(위 실측이 빈 상태였던 이유), 대시보드 상단 시스템 알림 영역을 렌더링할 때 사용합니다.
|
||||
|
||||
|
||||
### GET /api/admin/dashboard/recent-notifications
|
||||
<!-- @generated:start:api.admin.dashboard.recent-notifications -->
|
||||
- **라우트명**: `api.admin.dashboard.recent-notifications`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\DashboardController@recentNotifications`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.notification-logs.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/dashboard/recent-notifications HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`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) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "최근 알림을 성공적으로 조회했습니다.",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "apidoc.sample.event",
|
||||
"channel": "mail",
|
||||
"recipient": "API 문서 샘플 사용자",
|
||||
"subject": "API 문서 샘플 알림",
|
||||
"status": "sent",
|
||||
"time": "2시간 전",
|
||||
"timestamp": "2026-07-08T09:41:24+09:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.notification-logs.read`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 대시보드 "최근 알림" 카드에 표시할 최근 알림 발송 이력을 조회합니다. 인증(`auth:sanctum`)과 `core.notification-logs.read` 권한이 필요합니다. 각 항목은 알림 타입·채널·수신자·제목·상태와 상대 시간(`time`)·절대 시각(`timestamp`)을 포함하며, 전체 이력 목록(notification-logs)의 요약 뷰를 대시보드에 노출할 때 사용합니다.
|
||||
|
||||
|
||||
### GET /api/admin/dashboard/resources
|
||||
<!-- @generated:start:api.admin.dashboard.resources -->
|
||||
- **라우트명**: `api.admin.dashboard.resources`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\DashboardController@resources`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.dashboard.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/dashboard/resources HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`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 폴백 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "시스템 리소스 정보를 성공적으로 조회했습니다.",
|
||||
"data": {
|
||||
"cpu": {
|
||||
"percentage": 52,
|
||||
"color": "blue"
|
||||
},
|
||||
"memory": {
|
||||
"percentage": 86,
|
||||
"used": "27 GB",
|
||||
"total": "31.5 GB",
|
||||
"color": "yellow"
|
||||
},
|
||||
"disk": {
|
||||
"percentage": 76,
|
||||
"used": "362.6 GB",
|
||||
"total": "474.7 GB",
|
||||
"color": "yellow"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.dashboard.read`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 서버의 CPU·메모리·디스크 사용량을 조회합니다. 인증(`auth:sanctum`)과 `core.dashboard.read` 권한이 필요합니다. 각 항목은 사용률(`percentage`)과 상태 색상(`color`), 메모리·디스크의 경우 사용량/총량 문자열을 포함하며, 대시보드 시스템 리소스 게이지를 렌더링할 때 사용합니다.
|
||||
|
||||
|
||||
### GET /api/admin/dashboard/stats
|
||||
<!-- @generated:start:api.admin.dashboard.stats -->
|
||||
- **라우트명**: `api.admin.dashboard.stats`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\DashboardController@stats`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.dashboard.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/dashboard/stats HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`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: 전체 서비스 정상 동작 여부) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "대시보드 통계를 성공적으로 조회했습니다.",
|
||||
"data": {
|
||||
"total_users": {
|
||||
"count": 2,
|
||||
"change_percent": 0,
|
||||
"change_display": "+2",
|
||||
"trend": "up"
|
||||
},
|
||||
"installed_modules": {
|
||||
"total": 3,
|
||||
"active": 3
|
||||
},
|
||||
"active_plugins": {
|
||||
"total": 9,
|
||||
"active": 0
|
||||
},
|
||||
"installed_templates": {
|
||||
"total": 2,
|
||||
"active": 0
|
||||
},
|
||||
"language_packs": {
|
||||
"total": 17,
|
||||
"active": 1
|
||||
},
|
||||
"system_status": {
|
||||
"status": "normal",
|
||||
"label": "정상",
|
||||
"all_services_running": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.dashboard.read`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 대시보드 상단 통계 카드에 표시할 집계 데이터를 조회합니다. 인증(`auth:sanctum`)과 `core.dashboard.read` 권한이 필요합니다. 총 사용자 수(증감률 포함), 설치/활성 모듈·플러그인·템플릿·언어팩 수, 시스템 상태를 객체 형태로 반환하며, 대시보드 진입 시 요약 지표를 렌더링할 때 사용합니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.admin.extensions.auto-deactivated -->
|
||||
- **라우트명**: `api.admin.extensions.auto-deactivated`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\ExtensionRecoveryController@autoDeactivated`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.plugins.activate`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/extensions/auto-deactivated HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`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` | 현재 설치된 코어 버전 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "자동 비활성화된 확장 목록입니다.",
|
||||
"data": {
|
||||
"items": {
|
||||
"plugins": [],
|
||||
"modules": [],
|
||||
"templates": []
|
||||
},
|
||||
"current_core_version": "7.0.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.plugins.activate`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 코어 버전 비호환으로 자동 비활성화된 확장 목록을 타입별(`plugins`/`modules`/`templates`)로 반환합니다. 각 항목에는 식별자, 비호환 요구 버전, 비활성화 시각과 함께 현재 코어 버전이 담깁니다. 사용자가 dismiss한 알림과 hidden(학습용 샘플) 확장은 결과에서 제외됩니다. `core.plugins.activate` 권한이 필요하며, 상단 배너·대시보드 카드의 데이터 소스로 사용됩니다.
|
||||
|
||||
|
||||
### POST /api/admin/extensions/{type}/{identifier}/dismiss
|
||||
<!-- @generated:start:api.admin.extensions.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 | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/extensions/{type}/{identifier}/dismiss HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.plugins.activate`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 특정 확장의 호환성 알림을 현재 사용자 기준으로 dismiss(닫기) 처리합니다. 경로의 `{type}`(module|plugin|template)과 `{identifier}`로 대상을 지정하며, 해당 확장의 자동 비활성화 알림과 재호환 알림을 함께 dismiss합니다. `core.plugins.activate` 권한이 필요합니다. dismiss는 사용자별로 저장되므로, 캐시 만료나 감지 갱신 시 재호환 상태가 바뀌면 다시 노출될 수 있습니다.
|
||||
|
||||
|
||||
### POST /api/admin/extensions/{type}/{identifier}/recover
|
||||
<!-- @generated:start:api.admin.extensions.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 | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/extensions/{type}/{identifier}/recover HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.plugins.activate`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 코어와 재호환된 확장을 원클릭으로 복구(재활성화)합니다. 경로의 `{type}`/`{identifier}`로 대상을 지정하며, 대상이 `IncompatibleCore` 사유로 자동 비활성화된 상태인지 검증한 뒤 코어 버전 재검증을 거쳐 활성화합니다. 잘못된 타입은 422, 미존재 확장은 404, hidden 확장이나 자동 비활성화가 아닌 경우는 error_code와 함께 422를 반환하고, 재검증 실패 시 글로벌 핸들러가 core_version_mismatch로 변환합니다. `core.plugins.activate` 권한이 필요합니다.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,979 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.admin.language-packs.index -->
|
||||
- **라우트명**: `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부터 시작) |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/language-packs?scope=%EC%98%88%EC%8B%9C%EA%B0%92&target_identifier=example-key&locale=ko&status=%EC%98%88%EC%8B%9C%EA%B0%92&vendor=%EC%98%88%EC%8B%9C%EA%B0%92&search=%EC%98%88%EC%8B%9C%EA%B0%92&exclude_protected=1&per_page=1&page=1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "언어팩 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"id": null,
|
||||
"identifier": "g7-core-en",
|
||||
"vendor": "g7",
|
||||
"scope": "core",
|
||||
"target_identifier": null,
|
||||
"locale": "en",
|
||||
"locale_name": "EN",
|
||||
"locale_native_name": "English",
|
||||
"text_direction": "ltr",
|
||||
"version": "7.0.1",
|
||||
"latest_version": null,
|
||||
"target_version_constraint": null,
|
||||
"target_version_mismatch": false,
|
||||
"name": null,
|
||||
"license": null,
|
||||
"description": null,
|
||||
"status": "active",
|
||||
"is_protected": true,
|
||||
"source_type": "built_in",
|
||||
"origin": "built_in",
|
||||
"source_url": "lang/en",
|
||||
"github_url": null,
|
||||
"github_changelog_url": null,
|
||||
"bundled_identifier": null,
|
||||
"install_blocked_reason": null,
|
||||
"target_name": null,
|
||||
"installed_at": null,
|
||||
"activated_at": null,
|
||||
"created_at": null,
|
||||
"updated_at": null,
|
||||
"has_update": false,
|
||||
"abilities": {
|
||||
"can_activate": true,
|
||||
"can_deactivate": true,
|
||||
"can_uninstall": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"identifier": "apidoc-sample-lang",
|
||||
"vendor": "apidoc",
|
||||
"scope": "core",
|
||||
"target_identifier": null,
|
||||
"locale": "fr",
|
||||
"locale_name": "French",
|
||||
"locale_native_name": "Français",
|
||||
"text_direction": "ltr",
|
||||
"version": "1.0.0",
|
||||
"latest_version": "1.0.0",
|
||||
"target_version_constraint": null,
|
||||
"target_version_mismatch": false,
|
||||
"name": "API 문서 샘플 언어팩",
|
||||
"license": "MIT",
|
||||
"description": "문서 실측용 언어팩",
|
||||
"status": "active",
|
||||
"is_protected": false,
|
||||
"source_type": "bundled",
|
||||
"origin": "bundled",
|
||||
"source_url": null,
|
||||
"github_url": null,
|
||||
"github_changelog_url": null,
|
||||
"bundled_identifier": null,
|
||||
"install_blocked_reason": null,
|
||||
"target_name": null,
|
||||
"installed_at": "2026-07-05 10:41:24",
|
||||
"activated_at": "2026-07-05 10:41:24",
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 10:41:24",
|
||||
"has_update": false,
|
||||
"abilities": {
|
||||
"can_activate": true,
|
||||
"can_deactivate": true,
|
||||
"can_uninstall": true
|
||||
}
|
||||
},
|
||||
"... (총 25건 중 2건 표시)"
|
||||
],
|
||||
"meta": {
|
||||
"total": 43,
|
||||
"active": 15,
|
||||
"installed": 0,
|
||||
"inactive": 0,
|
||||
"error": 0,
|
||||
"uninstalled": 10,
|
||||
"current_page": 1,
|
||||
"last_page": 2,
|
||||
"per_page": 25
|
||||
},
|
||||
"abilities": {
|
||||
"can_install": true,
|
||||
"can_activate": true,
|
||||
"can_deactivate": true,
|
||||
"can_uninstall": true,
|
||||
"can_refresh_cache": true,
|
||||
"can_check_updates": true,
|
||||
"can_update": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.read`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 설치된 언어팩과 번들 소스로부터 노출되는 언어팩 목록을 페이지네이션으로 조회합니다. `core.language_packs.read` 권한이 필요합니다. `scope`/`locale`/`status`/`vendor`/`search` 등으로 필터링하고 `exclude_protected` 로 보호(protected) 팩을 제외할 수 있습니다. 관리자 언어팩 관리 화면의 목록/필터 표시에 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/language-packs/bulk-activate
|
||||
<!-- @generated:start: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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/bulk-activate HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ids": [
|
||||
"예시값"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.manage`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 전달된 `ids` 배열의 언어팩을 일괄 활성화합니다. `core.language_packs.manage` 권한이 필요합니다. 성공/실패를 분리한 결과를 반환하므로 일부만 실패해도 전체가 롤백되지 않습니다. 비활성 팩을 한 번에 재활성화하는 reactivate 모달의 "활성화" 동작에 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/language-packs/check-updates
|
||||
<!-- @generated:start: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`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/check-updates HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.update`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** GitHub 소스로 설치된 언어팩들의 원격 최신 버전을 조회해 업데이트 가능 여부를 확인합니다. `core.language_packs.update` 권한이 필요합니다. 실제 업데이트를 수행하지 않고 검사 결과(checked, updates, details)만 반환하며, 외부 GitHub 호출을 동반합니다. 언어팩 목록의 업데이트 배지 표시에 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/language-packs/install-from-bundled
|
||||
<!-- @generated:start: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 | 아니오 | — | 설치 후 자동 활성화 여부 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/install-from-bundled HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "example-key",
|
||||
"auto_activate": true
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.install`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** `lang-packs/_bundled/{identifier}` 디렉토리의 번들 소스에서 언어팩을 설치(또는 재설치)합니다. `core.language_packs.install` 권한이 필요합니다. `auto_activate` 가 true면 설치 후 곧바로 활성화합니다. 코어/확장에 선탑재된 번들 언어팩을 DB에 등록할 때 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/language-packs/install-from-file
|
||||
<!-- @generated:start: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 | 아니오 | — | 설치 후 자동 활성화 여부 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/install-from-file HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: multipart/form-data; boundary=----G7ExampleBoundary
|
||||
|
||||
------G7ExampleBoundary
|
||||
Content-Disposition: form-data; name="file"; filename="example.pdf"
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
(바이너리 파일 내용)
|
||||
------G7ExampleBoundary
|
||||
Content-Disposition: form-data; name="auto_activate"
|
||||
|
||||
1
|
||||
------G7ExampleBoundary--
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.install`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 업로드된 ZIP 파일에서 언어팩을 설치합니다. `core.language_packs.install` 권한이 필요합니다. manifest 검증에 실패하면 422로 응답하며, `auto_activate` 가 true면 설치 후 즉시 활성화합니다. 관리자가 로컬 ZIP 파일을 직접 업로드해 언어팩을 추가하는 화면에 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/language-packs/install-from-github
|
||||
<!-- @generated:start: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 | 아니오 | — | 설치 후 자동 활성화 여부 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/install-from-github HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"github_url": "https://example.com",
|
||||
"auto_activate": true
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.install`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 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
|
||||
<!-- @generated:start: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 | 아니오 | — | 설치 후 자동 활성화 여부 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/install-from-url HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"url": "https://example.com",
|
||||
"checksum": "예시값",
|
||||
"auto_activate": true
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.install`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 임의의 URL에서 언어팩 ZIP을 내려받아 설치합니다. `core.language_packs.install` 권한이 필요합니다. `checksum` 을 함께 전달하면 다운로드 무결성을 검증하며, manifest 검증 실패 시 422로 응답합니다. `auto_activate` 가 true면 설치 후 즉시 활성화합니다. GitHub 외 임의 호스팅에 배포된 언어팩을 설치할 때 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/language-packs/manifest-preview
|
||||
<!-- @generated:start: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 | 업로드 파일 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/manifest-preview HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: multipart/form-data; boundary=----G7ExampleBoundary
|
||||
|
||||
------G7ExampleBoundary
|
||||
Content-Disposition: form-data; name="file"; filename="example.pdf"
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
(바이너리 파일 내용)
|
||||
------G7ExampleBoundary--
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.install`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 업로드된 ZIP을 실제로 설치하지 않고 manifest 와 검증 결과만 미리 조회합니다. `core.language_packs.install` 권한이 필요합니다. 부수 효과 없이 읽기만 수행하며 검증 실패 시 422로 응답합니다. 설치 확인 모달에서 대상 언어팩의 메타데이터와 유효성을 미리 보여줄 때 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/language-packs/refresh-cache
|
||||
<!-- @generated:start: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`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/refresh-cache HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.manage`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 번역/레지스트리/템플릿 언어 캐시를 무효화합니다. `core.language_packs.manage` 권한이 필요합니다. 언어팩 파일을 직접 수정했거나 활성 상태가 프론트에 반영되지 않을 때 캐시를 강제로 갱신하는 용도로 사용합니다.
|
||||
|
||||
|
||||
### DELETE /api/admin/language-packs/{id}
|
||||
<!-- @generated:start:api.admin.language-packs.uninstall -->
|
||||
- **라우트명**: `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 | 아니오 | — | 연쇄 처리 여부 (의존 항목 함께 처리) |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/admin/language-packs/1?cascade=1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.manage`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 언어팩을 제거하고 설치된 파일을 삭제합니다. `core.language_packs.manage` 권한이 필요합니다. `cascade` 가 true면 연관 자원까지 함께 제거합니다. 대상이 존재하지 않으면 404로 응답합니다. 관리자 언어팩 관리 화면의 삭제(제거) 동작에 사용합니다.
|
||||
|
||||
|
||||
### GET /api/admin/language-packs/{id}
|
||||
<!-- @generated:start:api.admin.language-packs.show -->
|
||||
- **라우트명**: `api.admin.language-packs.show`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@show`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| id | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/language-packs/1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| identifier | string | `apidoc-sample-lang` | 언어팩 고유 식별자 ({vendor}-{scope}-{target?}-{locale}) |
|
||||
| vendor | string | `apidoc` | 언어팩 제작자 식별자 |
|
||||
| scope | string | `core` | 적용 대상 분류 |
|
||||
| target_identifier | null | `null` | 대상 확장 식별자 (scope=core일 때 null) |
|
||||
| locale | string | `fr` | IETF BCP-47 locale 태그 |
|
||||
| locale_name | string | `French` | 영문 언어명 |
|
||||
| locale_native_name | string | `Français` | 원어 언어명 |
|
||||
| text_direction | string | `ltr` | 텍스트 방향 |
|
||||
| version | string | `1.0.0` | 언어팩 버전 |
|
||||
| latest_version | string | `1.0.0` | 감지된 최신 배포 버전 |
|
||||
| target_version_constraint | null | `null` | 대상 확장 버전 제약 (semver) |
|
||||
| target_version_mismatch | boolean | `false` | 대상 버전 불일치 경고 플래그 |
|
||||
| name | string | `API 문서 샘플 언어팩` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| license | string | `MIT` | 라이선스 |
|
||||
| description | string | `문서 실측용 언어팩` | 설명 (다국어 필드는 로케일별 값 객체) |
|
||||
| status | string | `active` | 상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조) |
|
||||
| is_protected | boolean | `false` | protected 여부 |
|
||||
| source_type | string | `bundled` | 설치 소스 유형 (zip/github/url/bundled/bundled_with_extension) |
|
||||
| origin | string | `bundled` | 출처 (설치/등록 원천 구분 값) |
|
||||
| source_url | null | `null` | source URL |
|
||||
| github_url | null | `null` | GitHub 저장소 URL (manifest 파생) |
|
||||
| github_changelog_url | null | `null` | GitHub 변경 이력(CHANGELOG) URL (manifest 파생) |
|
||||
| bundled_identifier | null | `null` | 대응하는 번들 확장 식별자 (번들 원본 매칭용) |
|
||||
| install_blocked_reason | null | `null` | 설치가 차단된 사유 (차단 없으면 null) |
|
||||
| target_name | null | `null` | 대상 확장의 표시 이름 (scope+target_identifier 로 해석) |
|
||||
| installed_at | string | `2026-07-05 10:41:24` | installed 일시 |
|
||||
| activated_at | string | `2026-07-05 10:41:24` | activated 일시 |
|
||||
| created_at | string | `2026-07-08 10:41:24` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 10:41:24` | 최종 수정 일시 |
|
||||
| has_update | boolean | `false` | update 여부 |
|
||||
| abilities | object | `{"can_activate":true,"can_deactivate":true,"can_uninstall…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
| manifest | object | `{"name":{"en":"API Doc Sample Pack","ko":"API 문서 샘플 언어팩"}…` | language-pack.json 전체 스냅샷 |
|
||||
| validation_summary | object | `{"target_version_mismatch":false,"depends_on_core_locale"…` | 검증 결과 요약 객체 (통과/실패 건수 등) |
|
||||
| source_meta | object | `{"type":"bundled","url":null,"installed_by":6,"latest_ver…` | 원천 메타데이터 객체 (출처·경로·해석 정보) |
|
||||
| changelog_entries | array | `[]` | 변경 이력 항목 목록 (버전별 변경 내용) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "언어팩 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"identifier": "apidoc-sample-lang",
|
||||
"vendor": "apidoc",
|
||||
"scope": "core",
|
||||
"target_identifier": null,
|
||||
"locale": "fr",
|
||||
"locale_name": "French",
|
||||
"locale_native_name": "Français",
|
||||
"text_direction": "ltr",
|
||||
"version": "1.0.0",
|
||||
"latest_version": "1.0.0",
|
||||
"target_version_constraint": null,
|
||||
"target_version_mismatch": false,
|
||||
"name": "API 문서 샘플 언어팩",
|
||||
"license": "MIT",
|
||||
"description": "문서 실측용 언어팩",
|
||||
"status": "active",
|
||||
"is_protected": false,
|
||||
"source_type": "bundled",
|
||||
"origin": "bundled",
|
||||
"source_url": null,
|
||||
"github_url": null,
|
||||
"github_changelog_url": null,
|
||||
"bundled_identifier": null,
|
||||
"install_blocked_reason": null,
|
||||
"target_name": null,
|
||||
"installed_at": "2026-07-05 10:41:24",
|
||||
"activated_at": "2026-07-05 10:41:24",
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 10:41:24",
|
||||
"has_update": false,
|
||||
"abilities": {
|
||||
"can_activate": true,
|
||||
"can_deactivate": true,
|
||||
"can_uninstall": true
|
||||
},
|
||||
"manifest": {
|
||||
"name": {
|
||||
"en": "API Doc Sample Pack",
|
||||
"ko": "API 문서 샘플 언어팩"
|
||||
},
|
||||
"locale": "fr",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"validation_summary": {
|
||||
"target_version_mismatch": false,
|
||||
"depends_on_core_locale": null
|
||||
},
|
||||
"source_meta": {
|
||||
"type": "bundled",
|
||||
"url": null,
|
||||
"installed_by": 6,
|
||||
"latest_version": "1.0.0",
|
||||
"directory_path": "lang-packs/apidoc-sample-lang"
|
||||
},
|
||||
"changelog_entries": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.read`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 단일 언어팩의 상세 정보를 조회합니다. `core.language_packs.read` 권한이 필요합니다. `{id}` 가 정수면 DB 레코드를, 문자열(번들 식별자)이면 `lang-packs/_bundled/{id}` manifest 로 합성된 가상 행을 반환합니다. 미설치 번들 언어팩까지 상세 모달로 열람할 수 있도록 하며, 없으면 404로 응답합니다.
|
||||
|
||||
|
||||
### POST /api/admin/language-packs/{id}/activate
|
||||
<!-- @generated:start:api.admin.language-packs.activate -->
|
||||
- **라우트명**: `api.admin.language-packs.activate`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@activate`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.manage`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| id | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/1/activate HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.manage`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 언어팩을 활성화합니다(슬롯 스위칭). `core.language_packs.manage` 권한이 필요합니다. 동일 슬롯(scope·target·locale)에 이미 다른 활성 팩이 있으면 409(slot_conflict)로 현재/대상 팩을 함께 반환하며, 프론트는 확인 모달을 띄운 뒤 `force=true` 로 재호출해 교체합니다. 대상이 없으면 404로 응답합니다.
|
||||
|
||||
|
||||
### GET /api/admin/language-packs/{id}/changelog
|
||||
<!-- @generated:start:api.admin.language-packs.changelog -->
|
||||
- **라우트명**: `api.admin.language-packs.changelog`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@changelog`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| id | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/language-packs/1/changelog HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| identifier | string | `apidoc-sample-lang` | 언어팩 고유 식별자 ({vendor}-{scope}-{target?}-{locale}) |
|
||||
| entries | array | `[]` | 항목 목록 (각 원소는 대상 도메인 레코드) |
|
||||
| changelog | string | `` | 변경 이력 텍스트 (원격/파일 CHANGELOG 본문) |
|
||||
| has_changelog | boolean | `false` | changelog 여부 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "언어팩 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"identifier": "apidoc-sample-lang",
|
||||
"entries": [],
|
||||
"changelog": "",
|
||||
"has_changelog": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.read`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 언어팩의 CHANGELOG.md 내용을 반환합니다. `core.language_packs.read` 권한이 필요합니다. `{id}` 가 정수면 DB 레코드를, 문자열이면 번들 가상 행을 사용합니다. 파싱된 항목(entries), 원문(changelog), 존재 여부(has_changelog)를 함께 반환하며, CHANGELOG 파일이 없어도 빈 값으로 정상 응답합니다. 상세 모달의 변경 이력 탭에 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/language-packs/{id}/deactivate
|
||||
<!-- @generated:start:api.admin.language-packs.deactivate -->
|
||||
- **라우트명**: `api.admin.language-packs.deactivate`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@deactivate`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.manage`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| id | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/1/deactivate HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.manage`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 활성 언어팩을 비활성화합니다. `core.language_packs.manage` 권한이 필요합니다. 해당 슬롯의 번역 적용이 해제되며, 대상이 없으면 404로 응답합니다. 관리자 언어팩 관리 화면에서 활성 팩을 끄는 동작에 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/language-packs/{id}/update
|
||||
<!-- @generated:start:api.admin.language-packs.update -->
|
||||
- **라우트명**: `api.admin.language-packs.update`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\LanguagePackController@performUpdate`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.language_packs.update`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| id | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/1/update HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.language_packs.update`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** GitHub 소스 언어팩을 최신 버전으로 다시 내려받아 적용합니다. `core.language_packs.update` 권한이 필요합니다. 외부 GitHub 재다운로드와 파일 교체를 동반하며, 갱신된 언어팩 정보를 반환합니다. 대상이 없으면 404로 응답합니다. check-updates 로 업데이트가 감지된 팩을 실제로 갱신할 때 사용합니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.public.layouts.preview.serve -->
|
||||
- **라우트명**: `api.public.layouts.preview.serve`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Public\LayoutPreviewController@serve`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| token | path | string | 예 | — | 인증/검증 토큰 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/layouts/preview/{token}.json HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 편집 중인 레이아웃을 미리보기 토큰(UUID)으로 조회해 JSON으로 서빙합니다. 토큰으로 대상 레이아웃을 찾은 뒤 상속 병합과 확장(extension) 적용까지 마친 결과를 반환하며, 토큰이 유효하지 않으면 404를 반환합니다. 인증 미들웨어가 적용되지만 실질적 보안 메커니즘은 토큰 자체이며, 레이아웃 편집기에서 저장 전 변경분을 실제 렌더링으로 확인하는 용도입니다.
|
||||
|
||||
|
||||
### GET /api/layouts/{templateIdentifier}/{layoutName}.json
|
||||
<!-- @generated:start:api.public.layouts.serve -->
|
||||
- **라우트명**: `api.public.layouts.serve`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicLayoutController@serve`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| templateIdentifier | path | string | 예 | — | 대상 template의 식별자 |
|
||||
| layoutName | path | string | 예 | — | 대상 layout의 이름 (식별자) |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/layouts/{templateIdentifier}/{layoutName}.json HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 활성 템플릿의 병합된 레이아웃 JSON을 프론트엔드에 서빙합니다. 템플릿이 존재하고 활성 상태여야 하며, 상속 병합·확장 적용을 마친 결과를 ETag·Cache-Control 헤더와 함께 반환하고 미변경 시 304로 응답합니다. 레이아웃의 `permissions`에 따라 접근을 제한하고(비회원 401, 권한 부족 403), 컴포넌트 단위 권한 필터링을 사용자별로 적용합니다. 쿼리 `v`(정수 캐시 버전)로 캐시를 구분하며, `with_source_meta=1`은 `core.templates.layouts.edit` 권한이 있어야 노드별 출처 메타(편집기 전용)를 포함해 반환합니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.admin.license -->
|
||||
- **라우트명**: `api.admin.license`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\LicenseController@core`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/license HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| content | string | `프로그램 명칭 : 그누보드7 (Gnuboard7) 저작자 : (주…` | 본문 내용 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "성공적으로 처리되었습니다.",
|
||||
"data": {
|
||||
"content": "프로그램 명칭 : 그누보드7 (Gnuboard7)\n\n저작자 : (주)에스아이알소프트\n\n라이선스 (License)\n\n번역문 아래에 원문이 있습니다.\n\n주의)\n1. 번역문과 원문의 내용상 차이가 있는 경우 원문의 내용을 우선으로 따릅니다.\n2. 이 라이선스 파일 및 내용은 저작자를 제외한 어느 누구도 추가, 수정, 삭제할 수 없습니다.\n\n----- MIT 라이선스 (한국어 번역) --------------------------------------------------------\n\nMIT 라이선스\n\nCopyright (c) 2026 (주)에스아이알소프트\n\n이 소프트웨어와 관련 문서 파일(이하 \"소프트웨어\")의 복사본을 취득하는 모든 사람에게\n소프트웨어를 제한 없이 사용, 복사, 수정, 병합, 출판, 배포, 서브라이선스 허여 및/또는\n판매할 수 있는 권리를 무상으로 부여합니다. 다만, 소프트웨어를 제공받은 사람은 다음\n조건을 따라야 합니다:\n\n위 저작권 고지와 본 허가 고지는 소프트웨어의 모든 복사본 또는 상당 부분에 포함되어야\n합니다.\n\n소프트웨어는 \"있는 그대로\" 제공되며, 명시적이든 묵시적이든 어떠한 종류의 보증도 하지\n않습니다. 여기에는 상품성, 특정 목적에의 적합성 및 비침해에 대한 보증이 포함되나 이에\n국한되지 않습니다. 어떠한 경우에도 저작자 또는 저작권자는 소프트웨어나 소프트웨어의\n사용 또는 기타 거래로 인해 발생하는 계약, 불법행위 또는 기타 청구, 손해 또는 기타\n책임에 대해 책임을 지지 않습니다.\n\n----- MIT License (English Original) --------------------------------------------------------\n\nThe MIT License (MIT)\n\nCopyright (c) 2026 SIRSOFT\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 코어 라이선스 파일의 원문 텍스트를 `content`로 반환합니다. `auth:sanctum` 인증이 필요하며, 라이선스 파일이 없으면 404(`common.not_found`)를 반환합니다. 관리자 화면에서 코어 저작권·라이선스 고지를 표시하는 용도로 사용합니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.public.locales.active -->
|
||||
- **라우트명**: `api.public.locales.active`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Public\LocaleController@active`
|
||||
- **인증/권한**: 공개 (인증 불필요)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/locales/active HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| locales | array | `["ko","en","fr","ja"]` | 활성 로케일 코드 배열 |
|
||||
| locale_names | object | `{"ko":"한국어","en":"English","ja":"日本語","fr":"Français"}` | 로케일 코드별 표시명 맵 (config app.locale_names) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "locales.fetched",
|
||||
"data": {
|
||||
"locales": [
|
||||
"ko",
|
||||
"en",
|
||||
"fr"
|
||||
],
|
||||
"locale_names": {
|
||||
"ko": "한국어",
|
||||
"en": "English",
|
||||
"fr": "Français"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
_대표 에러 없음 (공개 조회). 인증·권한 미요구 엔드포인트로 도메인 특이 에러를 반환하지 않습니다._
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 현재 사이트가 즉시 노출 가능한 활성 로케일 목록과 로케일별 표시명(`locale_names`) 매핑을 반환합니다. 활성 코어 언어팩을 기준으로 산출되며 인증이 필요 없는 공개 엔드포인트입니다. 언어팩 설치·활성화 직후 사용자 언어 셀렉터를 새로고침 없이 갱신하는 시나리오에 사용합니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.me.destroy -->
|
||||
- **라우트명**: `api.me.destroy`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@destroy`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/me HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-500 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
현재 로그인한 사용자가 자신의 계정을 탈퇴한다. 프론트의 회원 탈퇴 모달(`_modal_withdraw.json`)이 호출한다. 별도 요청 파라미터 없이 인증 토큰의 사용자를 대상으로 하며, `UserService::withdrawUser()` 가 아바타·토큰 삭제와 개인정보 익명화를 수행한다. 되돌릴 수 없는 작업이므로 호출 전 사용자 확인 절차를 두는 것을 권장한다.
|
||||
|
||||
|
||||
### GET /api/me
|
||||
<!-- @generated:start:api.me.show -->
|
||||
- **라우트명**: `api.me.show`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@show`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/me HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`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) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "프로필 정보를 성공적으로 가져왔습니다.",
|
||||
"data": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 사용자",
|
||||
"nickname": "song.hyunji",
|
||||
"email": "apidoc-sample-user@example.com",
|
||||
"avatar": null,
|
||||
"language": "ko",
|
||||
"timezone": "Asia/Seoul",
|
||||
"country": "KR",
|
||||
"status": "active",
|
||||
"status_label": "활성",
|
||||
"status_variant": "success",
|
||||
"homepage": "https://example.com",
|
||||
"mobile": "010-9595-2897",
|
||||
"phone": "02-637-5618",
|
||||
"zipcode": "16505",
|
||||
"address": "경기도 안양시 봉은사로 2918",
|
||||
"address_detail": "48동 718호",
|
||||
"signature": "Fugit consequuntur repellendus sed.",
|
||||
"bio": "Ut magni et sunt ducimus error adipisci. Pariatur corporis voluptatem ratione quo non saepe. Illo atque praesentium possimus dolores qui est fugit. Sint fugiat numquam voluptates.",
|
||||
"is_super": false,
|
||||
"is_admin": true,
|
||||
"withdrawn_at": null,
|
||||
"last_login_at": "2026-07-07 10:41:24",
|
||||
"last_login_human": "1일 전",
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"is_owner": true,
|
||||
"notify_post_complete": false,
|
||||
"notify_post_reply": false,
|
||||
"notify_comment": false,
|
||||
"notify_reply_comment": false,
|
||||
"ecommerce_mileage": {
|
||||
"enabled": false
|
||||
},
|
||||
"ecommerce_preferred_currency": null,
|
||||
"ecommerce_preferred_shipping_country": null,
|
||||
"ecommerce_preferred_shipping_country_name": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
현재 로그인한 사용자의 프로필 정보를 조회한다. 프론트의 마이페이지(`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
|
||||
<!-- @generated:start:api.me.update -->
|
||||
- **라우트명**: `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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PUT /api/me HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "예시 이름",
|
||||
"nickname": "예시 이름",
|
||||
"email": "user@example.com",
|
||||
"password": "Password123!",
|
||||
"current_password": "Password123!",
|
||||
"language": "ko",
|
||||
"country": "KR",
|
||||
"timezone": "Asia/Seoul",
|
||||
"homepage": "https://example.com",
|
||||
"mobile": "010-1234-5678",
|
||||
"phone": "010-1234-5678",
|
||||
"zipcode": "06234",
|
||||
"address": "서울특별시 강남구 테헤란로 1",
|
||||
"address_detail": "서울특별시 강남구 테헤란로 1",
|
||||
"signature": "예시값",
|
||||
"bio": "예시 내용입니다.",
|
||||
"notify_post_complete": true,
|
||||
"notify_post_reply": true,
|
||||
"notify_comment": true,
|
||||
"notify_reply_comment": true
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
현재 로그인한 사용자가 자신의 프로필을 수정한다. 프론트의 프로필 수정 화면(`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` 형태로 반환된다.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,107 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.admin.notification-channels.index -->
|
||||
- **라우트명**: `api.admin.notification-channels.index`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationChannelController@index`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.settings.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/notification-channels HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`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` 훅으로 추가된 확장 채널이 병합됩니다. |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "알림 채널 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"channels": [
|
||||
{
|
||||
"id": "mail",
|
||||
"name_key": "notification.channels.mail.name",
|
||||
"icon": "fas fa-envelope",
|
||||
"description_key": "notification.channels.mail.description",
|
||||
"source": "core",
|
||||
"source_label_key": "notification.channels.core_default",
|
||||
"allow_guest": true,
|
||||
"name": "메일",
|
||||
"description": "이메일로 알림 발송",
|
||||
"source_label": "코어 기본 채널",
|
||||
"readiness": {
|
||||
"ready": false,
|
||||
"reason": "notification.readiness.mail_smtp_host_empty"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "database",
|
||||
"name_key": "notification.channels.database.name",
|
||||
"icon": "fas fa-bell",
|
||||
"description_key": "notification.channels.database.description",
|
||||
"source": "core",
|
||||
"source_label_key": "notification.channels.core_default",
|
||||
"allow_guest": false,
|
||||
"name": "사이트내 알림",
|
||||
"description": "사이트내 알림 센터에 표시",
|
||||
"source_label": "코어 기본 채널",
|
||||
"readiness": {
|
||||
"ready": true,
|
||||
"reason": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 시스템에서 사용 가능한 알림 채널 목록을 반환하며, 각 채널에 설정 완료 여부(`readiness`) 정보를 붙여 제공합니다. 인증(`auth:sanctum`)과 `core.settings.read` 권한이 필요합니다. 플러그인이 Filter 훅으로 채널을 확장할 수 있으므로 목록은 설치된 확장에 따라 달라집니다. 알림 정의·템플릿 편집 화면에서 채널 선택 옵션을 채우고 미설정 채널을 안내할 때 사용합니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,713 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.admin.notification-definitions.index -->
|
||||
- **라우트명**: `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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/notification-definitions?search=%EC%98%88%EC%8B%9C%EA%B0%92&extension_type=core&extension_identifier=example-key&channel=%EC%98%88%EC%8B%9C%EA%B0%92&is_active=1&per_page=1&sort_by=id&sort_order=asc HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "알림 정의 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"number": 1,
|
||||
"id": 1,
|
||||
"type": "apidoc-sample.event",
|
||||
"hook_prefix": "core",
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "",
|
||||
"name": {
|
||||
"ko": "API 문서 샘플 알림",
|
||||
"en": "API Doc Sample Notification"
|
||||
},
|
||||
"description": {
|
||||
"ko": "문서 실측용 알림 정의",
|
||||
"en": "Sample notification"
|
||||
},
|
||||
"variables": [],
|
||||
"channels": [
|
||||
"database",
|
||||
"mail"
|
||||
],
|
||||
"hooks": [],
|
||||
"is_active": true,
|
||||
"is_default": false,
|
||||
"templates": [
|
||||
{
|
||||
"id": 1,
|
||||
"definition_id": 1,
|
||||
"channel": "mail",
|
||||
"subject": "API 문서 샘플 템플릿 제목",
|
||||
"body": "안녕하세요 {{name}} 님, 문서 실측용 본문입니다.",
|
||||
"click_url": "/admin/apidoc-sample",
|
||||
"recipients": [
|
||||
{
|
||||
"type": "role",
|
||||
"value": "admin",
|
||||
"display_name": "관리자"
|
||||
}
|
||||
],
|
||||
"is_active": true,
|
||||
"is_default": false,
|
||||
"user_overrides": null,
|
||||
"updated_by": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 10:41:24",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 10:41:24",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"id": 2,
|
||||
"type": "order_confirmed",
|
||||
"hook_prefix": "sirsoft-ecommerce",
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce",
|
||||
"name": {
|
||||
"ko": "결제 완료",
|
||||
"en": "Payment Completed"
|
||||
},
|
||||
"description": {
|
||||
"ko": "결제 완료 시 고객에게 발송",
|
||||
"en": "Sent to customer when payment is completed"
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"key": "name",
|
||||
"description": "수신자 이름"
|
||||
},
|
||||
{
|
||||
"key": "app_name",
|
||||
"description": "사이트 이름"
|
||||
},
|
||||
{
|
||||
"key": "order_number",
|
||||
"description": "주문번호"
|
||||
},
|
||||
{
|
||||
"key": "total_amount",
|
||||
"description": "결제 금액"
|
||||
},
|
||||
{
|
||||
"key": "order_url",
|
||||
"description": "주문 상세 URL"
|
||||
},
|
||||
{
|
||||
"key": "site_url",
|
||||
"description": "사이트 URL"
|
||||
},
|
||||
{
|
||||
"key": "shipping_recipient_name",
|
||||
"description": "수취인 이름"
|
||||
},
|
||||
{
|
||||
"key": "shipping_country_name",
|
||||
"description": "배송국가명"
|
||||
},
|
||||
{
|
||||
"key": "shipping_address",
|
||||
"description": "배송지 주소"
|
||||
}
|
||||
],
|
||||
"channels": [
|
||||
"mail",
|
||||
"database"
|
||||
],
|
||||
"hooks": [
|
||||
"sirsoft-ecommerce.order.after_confirm"
|
||||
],
|
||||
"is_active": true,
|
||||
"is_default": true,
|
||||
"templates": [
|
||||
{
|
||||
"id": 2,
|
||||
"definition_id": 2,
|
||||
"channel": "mail",
|
||||
"subject": {
|
||||
"ko": "[{app_name}] 결제가 완료되었습니다 (주문번호: {order_number})",
|
||||
"en": "[{app_name}] Your payment has been completed (Order #{order_number})"
|
||||
},
|
||||
"body": {
|
||||
"ko": "<div style=\"font-family:'Malgun Gothic',sans-serif;max-width:600px;margin:0 auto;padding:20px\"><h2 style=\"color:#333;border-bottom:2px solid #4F46E5;padding-bottom:10px\">결제 완료</h2><p style=\"color:#555;line-height:1.6\">{name}님, 결제가 완료되었습니다. 주문해 주셔서 감사합니다.</p><div style=\"background:#f8f9fa;padding:15px;border-radius:8px;margin:15px 0\"><p style=\"margin:5px 0\"><strong>주문번호:</strong> {order_number}</p><p style=\"margin:5px 0\"><strong>결제금액:</strong> {total_amount}</p><p style=\"margin:5px 0\"><strong>받는분:</strong> {shipping_recipient_name}</p><p style=\"margin:5px 0\"><strong>배송국가:</strong> {shipping_country_name}</p><p style=\"margin:5px 0\"><strong>배송지:</strong> {shipping_address}</p></div><p style=\"color:#555;line-height:1.6\">주문 상세 내용은 아래 버튼을 클릭하여 확인하실 수 있습니다.</p><div style=\"text-align:center;margin:25px 0\"><a href=\"{order_url}\" style=\"display:inline-block;padding:12px 30px;background-color:#4F46E5;color:#ffffff;text-decoration:none;border-radius:6px;font-weight:bold\">주문 상세 보기</a></div><hr style=\"border:none;border-top:1px solid #eee;margin:20px 0\"><p style=\"color:#999;font-size:12px\">본 메일은 {app_name}에서 발송되었습니다.</p></div>",
|
||||
"en": "<div style=\"font-family:Arial,sans-serif;max-width:600px;margin:0 auto;padding:20px\"><h2 style=\"color:#333;border-bottom:2px solid #4F46E5;padding-bottom:10px\">Payment Completed</h2><p style=\"color:#555;line-height:1.6\">Dear {name}, your payment has been completed. Thank you for your order.</p><div style=\"background:#f8f9fa;padding:15px;border-radius:8px;margin:15px 0\"><p style=\"margin:5px 0\"><strong>Order Number:</strong> {order_number}</p><p style=\"margin:5px 0\"><strong>Total Amount:</strong> {total_amount}</p><p style=\"margin:5px 0\"><strong>Recipient:</strong> {shipping_recipient_name}</p><p style=\"margin:5px 0\"><strong>Shipping Country:</strong> {shipping_country_name}</p><p style=\"margin:5px 0\"><strong>Shipping Address:</strong> {shipping_address}</p></div><p style=\"color:#555;line-height:1.6\">Click the button below to view your order details.</p><div style=\"text-align:center;margin:25px 0\"><a href=\"{order_url}\" style=\"display:inline-block;padding:12px 30px;background-color:#4F46E5;color:#ffffff;text-decoration:none;border-radius:6px;font-weight:bold\">View Order</a></div><hr style=\"border:none;border-top:1px solid #eee;margin:20px 0\"><p style=\"color:#999;font-size:12px\">This email was sent from {app_name}.</p></div>"
|
||||
},
|
||||
"click_url": null,
|
||||
"recipients": [
|
||||
{
|
||||
"type": "trigger_user"
|
||||
}
|
||||
],
|
||||
"is_active": true,
|
||||
"is_default": true,
|
||||
"user_overrides": null,
|
||||
"updated_by": null,
|
||||
"created_at": "2026-07-08 10:43:32",
|
||||
"updated_at": "2026-07-08 10:43:32",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"definition_id": 2,
|
||||
"channel": "database",
|
||||
"subject": {
|
||||
"ko": "결제가 완료되었습니다",
|
||||
"en": "Your payment has been completed"
|
||||
},
|
||||
"body": {
|
||||
"ko": "{name}님, 주문번호 {order_number}의 결제가 완료되었습니다.",
|
||||
"en": "{name}, payment for your order {order_number} has been completed."
|
||||
},
|
||||
"click_url": "{order_url}",
|
||||
"recipients": [
|
||||
{
|
||||
"type": "trigger_user"
|
||||
}
|
||||
],
|
||||
"is_active": true,
|
||||
"is_default": true,
|
||||
"user_overrides": null,
|
||||
"updated_by": null,
|
||||
"created_at": "2026-07-08 10:43:32",
|
||||
"updated_at": "2026-07-08 10:43:32",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-08 10:43:32",
|
||||
"updated_at": "2026-07-08 10:43:32",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
},
|
||||
"... (총 18건 중 2건 표시)"
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"per_page": 25,
|
||||
"total": 18,
|
||||
"from": 1,
|
||||
"to": 18,
|
||||
"has_more_pages": false
|
||||
},
|
||||
"abilities": {
|
||||
"can_update": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 등록된 알림 정의 목록을 페이지네이션으로 조회합니다. 인증(`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}
|
||||
<!-- @generated:start:api.admin.notification-definitions.show -->
|
||||
- **라우트명**: `api.admin.notification-definitions.show`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationDefinitionController@show`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.settings.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| definition | path | string | 예 | — | 대상 definition의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/notification-definitions/1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "알림 정의를 조회했습니다.",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"type": "apidoc-sample.event",
|
||||
"hook_prefix": "core",
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "",
|
||||
"name": {
|
||||
"ko": "API 문서 샘플 알림",
|
||||
"en": "API Doc Sample Notification"
|
||||
},
|
||||
"description": {
|
||||
"ko": "문서 실측용 알림 정의",
|
||||
"en": "Sample notification"
|
||||
},
|
||||
"variables": [],
|
||||
"channels": [
|
||||
"database",
|
||||
"mail"
|
||||
],
|
||||
"hooks": [],
|
||||
"is_active": true,
|
||||
"is_default": false,
|
||||
"templates": [
|
||||
{
|
||||
"id": 1,
|
||||
"definition_id": 1,
|
||||
"channel": "mail",
|
||||
"subject": "API 문서 샘플 템플릿 제목",
|
||||
"body": "안녕하세요 {{name}} 님, 문서 실측용 본문입니다.",
|
||||
"click_url": "/admin/apidoc-sample",
|
||||
"recipients": [
|
||||
{
|
||||
"type": "role",
|
||||
"value": "admin",
|
||||
"display_name": "관리자"
|
||||
}
|
||||
],
|
||||
"is_active": true,
|
||||
"is_default": false,
|
||||
"user_overrides": null,
|
||||
"updated_by": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 10:41:24",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 10:41:24",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 단일 알림 정의의 상세 정보를 조회하며, 응답에 소속 템플릿(`templates`)을 함께 로드합니다. 인증(`auth:sanctum`)과 `core.settings.read` 권한이 필요합니다. `definition` 경로 파라미터로 대상을 지정하며, 정의 편집 화면 진입 시 채널별 템플릿을 포함한 전체 구성을 불러올 때 사용합니다.
|
||||
|
||||
|
||||
### PUT /api/admin/notification-definitions/{definition}
|
||||
<!-- @generated:start:api.admin.notification-definitions.update -->
|
||||
- **라우트명**: `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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PUT /api/admin/notification-definitions/1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"channels": [
|
||||
"예시값"
|
||||
],
|
||||
"hooks": [
|
||||
"예시값"
|
||||
],
|
||||
"is_active": true
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 알림 정의의 활성 채널(`channels`), 트리거 훅(`hooks`), 활성 상태(`is_active`)를 수정합니다. 인증(`auth:sanctum`)과 `core.settings.update` 권한이 필요합니다. Service 계층에서 수정 후 템플릿을 다시 로드해 반환하며, 확장이 `core.notification_definition.filter_update_rules` 훅으로 추가 파라미터를 검증에 넣을 수 있습니다. 발송 채널 구성이나 훅 연결을 변경할 때 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/notification-definitions/{definition}/reset
|
||||
<!-- @generated:start:api.admin.notification-definitions.reset -->
|
||||
- **라우트명**: `api.admin.notification-definitions.reset`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationDefinitionController@reset`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.settings.update`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| definition | path | string | 예 | — | 대상 definition의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/notification-definitions/1/reset HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| type | string | `apidoc-sample.event` | 알림 타입 (welcome, order_confirmed 등) |
|
||||
| hook_prefix | string | `core` | 훅 접두사 (core.auth, sirsoft-ecommerce 등) |
|
||||
| extension_type | string | `core` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) |
|
||||
| extension_identifier | string | `` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| name | object | `{"ko":"API 문서 샘플 알림","en":"API Doc Sample Notification"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| description | object | `{"ko":"문서 실측용 알림 정의","en":"Sample notification"}` | 설명 (다국어 필드는 로케일별 값 객체) |
|
||||
| variables | array | `[]` | 사용 가능 변수 메타데이터 ([{key, description}]) |
|
||||
| channels | array | `["database","mail"]` | 활성 채널 (["mail", "database"]) |
|
||||
| hooks | array | `[]` | 트리거 훅 목록 (["core.auth.after_register"]) |
|
||||
| is_active | boolean | `true` | active 여부 |
|
||||
| is_default | boolean | `true` | default 여부 |
|
||||
| templates | array | `[{"id":1,"definition_id":1,"channel":"mail","subject":"AP…` | 템플릿 목록 (각 원소 identifier/name 등 — 템플릿 관계 파생) |
|
||||
| created_at | string | `2026-07-08 10:41:24` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 12:14:43` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "알림 정의의 모든 템플릿이 기본값으로 복원되었습니다.",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"type": "apidoc-sample.event",
|
||||
"hook_prefix": "core",
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "",
|
||||
"name": {
|
||||
"ko": "API 문서 샘플 알림",
|
||||
"en": "API Doc Sample Notification"
|
||||
},
|
||||
"description": {
|
||||
"ko": "문서 실측용 알림 정의",
|
||||
"en": "Sample notification"
|
||||
},
|
||||
"variables": [],
|
||||
"channels": [
|
||||
"database",
|
||||
"mail"
|
||||
],
|
||||
"hooks": [],
|
||||
"is_active": true,
|
||||
"is_default": true,
|
||||
"templates": [
|
||||
{
|
||||
"id": 1,
|
||||
"definition_id": 1,
|
||||
"channel": "mail",
|
||||
"subject": "API 문서 샘플 템플릿 제목",
|
||||
"body": "안녕하세요 {{name}} 님, 문서 실측용 본문입니다.",
|
||||
"click_url": "/admin/apidoc-sample",
|
||||
"recipients": [
|
||||
{
|
||||
"type": "role",
|
||||
"value": "admin",
|
||||
"display_name": "관리자"
|
||||
}
|
||||
],
|
||||
"is_active": true,
|
||||
"is_default": false,
|
||||
"user_overrides": null,
|
||||
"updated_by": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 10:41:24",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 12:14:43",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 알림 정의에 속한 모든 채널 템플릿을 기본값(default) 데이터로 일괄 복원하고, 정의 자체를 default 상태로 표시합니다. 인증(`auth:sanctum`)과 `core.settings.update` 권한이 필요합니다. 각 템플릿의 제목·본문을 기본값으로 덮어쓰는 파괴적 작업이므로 사용자 편집분이 사라집니다. 관리자가 커스터마이징한 알림 문구를 초기 상태로 되돌릴 때 사용합니다.
|
||||
|
||||
|
||||
### PATCH /api/admin/notification-definitions/{definition}/toggle-active
|
||||
<!-- @generated:start:api.admin.notification-definitions.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의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PATCH /api/admin/notification-definitions/1/toggle-active HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| type | string | `apidoc-sample.event` | 알림 타입 (welcome, order_confirmed 등) |
|
||||
| hook_prefix | string | `core` | 훅 접두사 (core.auth, sirsoft-ecommerce 등) |
|
||||
| extension_type | string | `core` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) |
|
||||
| extension_identifier | string | `` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| name | object | `{"ko":"API 문서 샘플 알림","en":"API Doc Sample Notification"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| description | object | `{"ko":"문서 실측용 알림 정의","en":"Sample notification"}` | 설명 (다국어 필드는 로케일별 값 객체) |
|
||||
| variables | array | `[]` | 사용 가능 변수 메타데이터 ([{key, description}]) |
|
||||
| channels | array | `["database","mail"]` | 활성 채널 (["mail", "database"]) |
|
||||
| hooks | array | `[]` | 트리거 훅 목록 (["core.auth.after_register"]) |
|
||||
| is_active | boolean | `false` | active 여부 |
|
||||
| is_default | boolean | `false` | default 여부 |
|
||||
| templates | null | `null` | 템플릿 목록 (각 원소 identifier/name 등 — 템플릿 관계 파생) |
|
||||
| created_at | string | `2026-07-08 10:41:24` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 12:14:43` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "알림 정의 활성 상태가 변경되었습니다.",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"type": "apidoc-sample.event",
|
||||
"hook_prefix": "core",
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "",
|
||||
"name": {
|
||||
"ko": "API 문서 샘플 알림",
|
||||
"en": "API Doc Sample Notification"
|
||||
},
|
||||
"description": {
|
||||
"ko": "문서 실측용 알림 정의",
|
||||
"en": "Sample notification"
|
||||
},
|
||||
"variables": [],
|
||||
"channels": [
|
||||
"database",
|
||||
"mail"
|
||||
],
|
||||
"hooks": [],
|
||||
"is_active": false,
|
||||
"is_default": false,
|
||||
"templates": null,
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 12:14:43",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 알림 정의의 활성 상태(`is_active`)를 현재 값의 반대로 토글합니다. 인증(`auth:sanctum`)과 `core.settings.update` 권한이 필요합니다. 비활성 정의는 해당 알림 발송이 중단되므로, 관리자가 목록에서 특정 알림을 켜거나 끄는 스위치 조작에 사용합니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.admin.notification-logs.index -->
|
||||
- **라우트명**: `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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/notification-logs?sender_user_id=1&recipient_user_id=1&search=%EC%98%88%EC%8B%9C%EA%B0%92&channel=%EC%98%88%EC%8B%9C%EA%B0%92¬ification_type=%EC%98%88%EC%8B%9C%EA%B0%92&extension_type=core&status=%EC%98%88%EC%8B%9C%EA%B0%92&per_page=1&sort_by=id&sort_order=asc HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "알림 발송 이력을 조회했습니다.",
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"number": 1,
|
||||
"id": 1,
|
||||
"channel": "mail",
|
||||
"notification_type": "apidoc.sample.event",
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "",
|
||||
"recipient_user_id": 6,
|
||||
"recipient_identifier": "apidoc-sample-user@example.com",
|
||||
"recipient_name": "API 문서 샘플 사용자",
|
||||
"sender_user_id": 6,
|
||||
"sender": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 사용자",
|
||||
"email": "apidoc-sample-user@example.com"
|
||||
},
|
||||
"recipient": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 사용자",
|
||||
"email": "apidoc-sample-user@example.com"
|
||||
},
|
||||
"subject": "API 문서 샘플 알림",
|
||||
"body": "문서 실측용 알림 본문입니다.",
|
||||
"status": "sent",
|
||||
"error_message": null,
|
||||
"source": "apidoc",
|
||||
"sent_at": "2026-07-08 09:41:24",
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 10:41:24",
|
||||
"abilities": {
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"per_page": 25,
|
||||
"total": 1,
|
||||
"from": 1,
|
||||
"to": 1,
|
||||
"has_more_pages": false
|
||||
},
|
||||
"abilities": {
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.notification-logs.read`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 알림 발송 이력을 페이지네이션으로 조회합니다. 인증(`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
|
||||
<!-- @generated:start:api.admin.notification-logs.bulk-destroy -->
|
||||
- **라우트명**: `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 | 대상 리소스 식별자 배열 (대량 작업 대상) |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/notification-logs/bulk-delete HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ids": [
|
||||
"예시값"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.notification-logs.delete`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 알림 발송 이력을 ID 배열(`ids`)로 다건 삭제하고 삭제 건수(`deleted_count`)를 반환합니다. 인증(`auth:sanctum`)과 `core.notification-logs.delete` 권한이 필요합니다. 복구 불가능한 삭제이므로 주의가 필요하며, 관리자가 목록에서 여러 이력을 선택해 일괄 정리할 때 사용합니다.
|
||||
|
||||
|
||||
### DELETE /api/admin/notification-logs/{notificationLog}
|
||||
<!-- @generated:start:api.admin.notification-logs.destroy -->
|
||||
- **라우트명**: `api.admin.notification-logs.destroy`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationLogController@destroy`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.notification-logs.delete`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| notificationLog | path | string | 예 | — | 대상 notification log의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/admin/notification-logs/1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
|
||||
|
||||
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "알림 발송 이력이 삭제되었습니다.",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.notification-logs.delete`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 단일 알림 발송 이력을 삭제합니다. 인증(`auth:sanctum`)과 `core.notification-logs.delete` 권한이 필요합니다. `notificationLog` 경로 파라미터로 대상을 지정하며, 복구 불가능한 삭제입니다. 관리자가 개별 발송 이력 한 건을 제거할 때 사용합니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
# 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
|
||||
<!-- @generated:start: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 | 로케일 코드 (표시 언어/지역) |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/notification-templates/preview HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"definition_id": 1,
|
||||
"subject": [
|
||||
"예시값"
|
||||
],
|
||||
"body": [
|
||||
"예시값"
|
||||
],
|
||||
"locale": "ko"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 저장 전 알림 템플릿의 렌더링 결과를 미리 확인합니다. `definition_id` 와 다국어 `subject`/`body`, 선택적 `locale` 을 받아 샘플 변수로 치환된 제목·본문을 반환합니다. 인증(`auth:sanctum`)과 `core.settings.read` 권한이 필요하며, 실제 발송이나 저장은 일어나지 않습니다. 템플릿 편집 화면에서 변수 치환 결과를 실시간으로 확인할 때 사용합니다.
|
||||
|
||||
|
||||
### PUT /api/admin/notification-templates/{template}
|
||||
<!-- @generated:start:api.admin.notification-templates.update -->
|
||||
- **라우트명**: `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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PUT /api/admin/notification-templates/1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"subject": [
|
||||
"예시값"
|
||||
],
|
||||
"body": [
|
||||
"예시값"
|
||||
],
|
||||
"click_url": "https://example.com",
|
||||
"recipients": [
|
||||
"예시값"
|
||||
],
|
||||
"is_active": true
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 단일 채널 알림 템플릿의 다국어 제목(`subject`)·본문(`body`)과 클릭 URL, 수신자(`recipients`), 활성 상태를 수정합니다. 인증(`auth:sanctum`)과 `core.settings.update` 권한이 필요합니다. `template` 경로 파라미터로 대상을 지정하며, 확장이 `core.notification_template.filter_update_rules` 훅으로 추가 파라미터를 검증에 넣을 수 있습니다. 관리자가 특정 채널의 알림 문구를 편집해 저장할 때 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/notification-templates/{template}/reset
|
||||
<!-- @generated:start:api.admin.notification-templates.reset -->
|
||||
- **라우트명**: `api.admin.notification-templates.reset`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationTemplateController@reset`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.settings.update`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| template | path | string | 예 | — | 대상 template의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/notification-templates/1/reset HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 단일 채널 템플릿을 소속 정의의 기본값 데이터로 복원합니다. 인증(`auth:sanctum`)과 `core.settings.update` 권한이 필요합니다. 소속 정의가 없으면 404, 해당 채널의 기본 데이터가 없으면 404 를 반환합니다. 편집한 문구를 버리고 기본값 하나만 되돌릴 때 사용하며, 정의 전체를 복원하는 정의 reset 과 달리 대상 템플릿에만 적용됩니다.
|
||||
|
||||
|
||||
### PATCH /api/admin/notification-templates/{template}/toggle-active
|
||||
<!-- @generated:start:api.admin.notification-templates.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의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PATCH /api/admin/notification-templates/1/toggle-active HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| definition_id | integer | `1` | definition 식별자 (연관 리소스 참조) |
|
||||
| channel | string | `mail` | 채널: mail, database, fcm |
|
||||
| subject | string | `API 문서 샘플 템플릿 제목` | 다국어 제목 ({"ko": "...", "en": "..."}) |
|
||||
| body | string | `안녕하세요 {{name}} 님, 문서 실측용 본문입니다.` | 다국어 본문 ({"ko": "...", "en": "..."}) |
|
||||
| click_url | string | `/admin/apidoc-sample` | click URL |
|
||||
| recipients | array | `[{"type":"role","value":"admin","display_name":"관리자"}]` | 수신자 규칙 JSON ([{type, value, relation, exclude_trigger_user}]) |
|
||||
| is_active | boolean | `false` | active 여부 |
|
||||
| is_default | boolean | `false` | default 여부 |
|
||||
| user_overrides | array | `["is_active"]` | 사용자가 수정한 필드명 목록 |
|
||||
| updated_by | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 최종 수정한 사용자 정보 (uuid/name — updated_by 관계 파생, 없으면 null) |
|
||||
| created_at | string | `2026-07-08 10:41:24` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 12:14:43` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "알림 템플릿 활성 상태가 변경되었습니다.",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"definition_id": 1,
|
||||
"channel": "mail",
|
||||
"subject": "API 문서 샘플 템플릿 제목",
|
||||
"body": "안녕하세요 {{name}} 님, 문서 실측용 본문입니다.",
|
||||
"click_url": "/admin/apidoc-sample",
|
||||
"recipients": [
|
||||
{
|
||||
"type": "role",
|
||||
"value": "admin",
|
||||
"display_name": "관리자"
|
||||
}
|
||||
],
|
||||
"is_active": false,
|
||||
"is_default": false,
|
||||
"user_overrides": [
|
||||
"is_active"
|
||||
],
|
||||
"updated_by": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 12:14:43",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 단일 채널 알림 템플릿의 활성 상태(`is_active`)를 현재 값의 반대로 토글합니다. 인증(`auth:sanctum`)과 `core.settings.update` 권한이 필요합니다. 비활성 템플릿은 해당 채널로의 발송이 중단되므로, 정의는 유지한 채 특정 채널만 켜거나 끌 때 사용합니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,780 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.admin.notifications.index -->
|
||||
- **라우트명**: `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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/notifications?read=unread&per_page=1&page=1&sort_order=asc HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
|
||||
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "알림 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"data": [],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"per_page": 25,
|
||||
"total": 0,
|
||||
"from": null,
|
||||
"to": null,
|
||||
"has_more_pages": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
인증된 관리자 본인의 사이트내 알림 목록을 최신순(`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
|
||||
<!-- @generated:start:api.admin.notifications.destroy-all -->
|
||||
- **라우트명**: `api.admin.notifications.destroy-all`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationController@destroyAll`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.notifications.delete`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/admin/notifications/all HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| deleted_count | integer | `0` | deleted 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "모든 알림이 삭제되었습니다.",
|
||||
"data": {
|
||||
"deleted_count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.notifications.delete`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
인증된 관리자 본인의 모든 사이트내 알림(읽음·미읽음 무관)을 삭제합니다. 삭제 대상은 요청 사용자 본인의 알림으로만 한정되며, 응답 `data.deleted_count` 에 삭제된 건수를 반환합니다. 되돌릴 수 없는 작업이므로 UI 에서 확인 절차를 거친 뒤 호출합니다.
|
||||
|
||||
|
||||
### POST /api/admin/notifications/read-all
|
||||
<!-- @generated:start:api.admin.notifications.read-all -->
|
||||
- **라우트명**: `api.admin.notifications.read-all`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationController@markAllAsRead`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.notifications.update`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/notifications/read-all HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| marked_count | integer | `0` | marked 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "모든 알림을 읽음 처리했습니다.",
|
||||
"data": {
|
||||
"marked_count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.notifications.update`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
인증된 관리자 본인의 미읽음 알림을 모두 읽음 처리합니다. 처리 대상 `read_at` 을 현재 시각으로 갱신하며, 응답 `data.marked_count` 에 읽음 처리된 건수를 반환합니다. 이미 읽음 상태인 알림은 대상에서 제외됩니다.
|
||||
|
||||
|
||||
### POST /api/admin/notifications/read-batch
|
||||
<!-- @generated:start: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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/notifications/read-batch HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ids": [
|
||||
"예시값"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.notifications.update`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
`ids` 배열로 지정한 알림들을 일괄 읽음 처리합니다. 처리 대상은 요청 사용자 본인의 미읽음 알림 중 지정된 ID 에 해당하는 것으로 한정되며, 이미 읽음 상태이거나 본인 소유가 아닌 ID 는 무시됩니다. 응답 `data.marked_count` 에 실제 읽음 처리된 건수를 반환합니다. `ids` 는 최소 1개, 최대 100개까지 전달할 수 있습니다.
|
||||
|
||||
|
||||
### GET /api/admin/notifications/unread-count
|
||||
<!-- @generated:start:api.admin.notifications.unread-count -->
|
||||
- **라우트명**: `api.admin.notifications.unread-count`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationController@unreadCount`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.notifications.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/notifications/unread-count HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| unread_count | integer | `0` | unread 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "미읽음 알림 수를 조회했습니다.",
|
||||
"data": {
|
||||
"unread_count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
인증된 관리자 본인의 미읽음 알림 개수를 집계해 `data.unread_count` 로 반환합니다. `_admin_base.json` 헤더 알림 벨의 미읽음 배지에 사용되며, WebSocket 으로 새 알림이 수신되면 이 값을 재조회해 갱신합니다.
|
||||
|
||||
|
||||
### DELETE /api/admin/notifications/{notification}
|
||||
<!-- @generated:start:api.admin.notifications.destroy -->
|
||||
- **라우트명**: `api.admin.notifications.destroy`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationController@destroy`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.notifications.delete`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| notification | path | string | 예 | — | 대상 notification의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/admin/notifications/{notification} HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.notifications.delete`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
경로의 `{notification}` ID 에 해당하는 알림 1건을 삭제합니다. 대상은 요청 사용자 본인의 알림으로 한정되며, 해당 ID 가 본인 소유로 존재하지 않으면 404(`notification.user.not_found`)를 반환합니다.
|
||||
|
||||
|
||||
### PATCH /api/admin/notifications/{notification}/read
|
||||
<!-- @generated:start:api.admin.notifications.read -->
|
||||
- **라우트명**: `api.admin.notifications.read`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\NotificationController@markAsRead`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.notifications.update`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| notification | path | string | 예 | — | 대상 notification의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PATCH /api/admin/notifications/{notification}/read HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.notifications.update`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
경로의 `{notification}` ID 에 해당하는 알림 1건을 읽음 처리하고, 갱신된 알림 리소스를 `data` 로 반환합니다. 대상은 요청 사용자(관리자) 본인의 알림으로 한정되며, 본인 소유로 존재하지 않으면 404(`notification.user.not_found`)를 반환합니다. 반환 리소스에는 `id`, `type`, `type_label`, `subject`, `body`, `url`, `read_at`, `created_at` 가 포함됩니다.
|
||||
|
||||
|
||||
### GET /api/user/notifications
|
||||
<!-- @generated:start:api.user.notifications.index -->
|
||||
- **라우트명**: `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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/user/notifications?read=unread&per_page=1&page=1&sort_order=asc HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
|
||||
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "알림 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"data": [],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"per_page": 25,
|
||||
"total": 0,
|
||||
"from": null,
|
||||
"to": null,
|
||||
"has_more_pages": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`core.user-notifications.read`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
인증된 사용자 본인의 사이트내 알림 목록을 최신순(`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
|
||||
<!-- @generated:start:api.user.notifications.destroy-all -->
|
||||
- **라우트명**: `api.user.notifications.destroy-all`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Auth\NotificationController@destroyAll`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.user-notifications.delete`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/user/notifications/all HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| deleted_count | integer | `0` | deleted 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "모든 알림이 삭제되었습니다.",
|
||||
"data": {
|
||||
"deleted_count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`core.user-notifications.delete`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
인증된 사용자 본인의 모든 사이트내 알림(읽음·미읽음 무관)을 삭제합니다. 삭제 대상은 요청 사용자 본인의 알림으로만 한정되며, 응답 `data.deleted_count` 에 삭제된 건수를 반환합니다. 되돌릴 수 없는 작업입니다.
|
||||
|
||||
|
||||
### POST /api/user/notifications/read-all
|
||||
<!-- @generated:start:api.user.notifications.read-all -->
|
||||
- **라우트명**: `api.user.notifications.read-all`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Auth\NotificationController@markAllAsRead`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.user-notifications.update`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/user/notifications/read-all HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| marked_count | integer | `0` | marked 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "모든 알림을 읽음 처리했습니다.",
|
||||
"data": {
|
||||
"marked_count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`core.user-notifications.update`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
인증된 사용자 본인의 미읽음 알림을 모두 읽음 처리합니다. 처리 대상 `read_at` 을 현재 시각으로 갱신하며, 응답 `data.marked_count` 에 읽음 처리된 건수를 반환합니다. 이미 읽음 상태인 알림은 대상에서 제외됩니다.
|
||||
|
||||
|
||||
### POST /api/user/notifications/read-batch
|
||||
<!-- @generated:start: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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/user/notifications/read-batch HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ids": [
|
||||
"예시값"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`core.user-notifications.update`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
`ids` 배열로 지정한 알림들을 일괄 읽음 처리합니다. 처리 대상은 요청 사용자 본인의 미읽음 알림 중 지정된 ID 에 해당하는 것으로 한정되며, 이미 읽음 상태이거나 본인 소유가 아닌 ID 는 무시됩니다. 응답 `data.marked_count` 에 실제 읽음 처리된 건수를 반환합니다. `ids` 는 최소 1개, 최대 100개까지 전달할 수 있습니다.
|
||||
|
||||
|
||||
### GET /api/user/notifications/unread-count
|
||||
<!-- @generated:start:api.user.notifications.unread-count -->
|
||||
- **라우트명**: `api.user.notifications.unread-count`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Auth\NotificationController@unreadCount`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.user-notifications.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/user/notifications/unread-count HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| unread_count | integer | `0` | unread 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "미읽음 알림 수를 조회했습니다.",
|
||||
"data": {
|
||||
"unread_count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`core.user-notifications.read`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
인증된 사용자 본인의 미읽음 알림 개수를 집계해 `data.unread_count` 로 반환합니다. `_user_base.json` 의 알림 미읽음 배지에 사용되며, WebSocket 으로 새 알림이 수신되면 이 값을 재조회해 갱신합니다.
|
||||
|
||||
|
||||
### DELETE /api/user/notifications/{notification}
|
||||
<!-- @generated:start:api.user.notifications.destroy -->
|
||||
- **라우트명**: `api.user.notifications.destroy`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Auth\NotificationController@destroy`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.user-notifications.delete`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| notification | path | string | 예 | — | 대상 notification의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/user/notifications/{notification} HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`core.user-notifications.delete`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
경로의 `{notification}` ID 에 해당하는 알림 1건을 삭제합니다. 대상은 요청 사용자 본인의 알림으로 한정되며, 해당 ID 가 본인 소유로 존재하지 않으면 404(`notification.user.not_found`)를 반환합니다.
|
||||
|
||||
|
||||
### PATCH /api/user/notifications/{notification}/read
|
||||
<!-- @generated:start:api.user.notifications.read -->
|
||||
- **라우트명**: `api.user.notifications.read`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Auth\NotificationController@markAsRead`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.user-notifications.update`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| notification | path | string | 예 | — | 대상 notification의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PATCH /api/user/notifications/{notification}/read HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`core.user-notifications.update`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
경로의 `{notification}` ID 에 해당하는 알림 1건을 읽음 처리하고, 갱신된 알림 리소스를 `data` 로 반환합니다. 대상은 요청 사용자 본인의 알림으로 한정되며, 본인 소유로 존재하지 않으면 404(`notification.user.not_found`)를 반환합니다. 반환 리소스에는 `id`, `type`, `type_label`, `subject`, `body`, `url`, `read_at`, `created_at` 가 포함됩니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# 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
|
||||
<!-- @generated:start: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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PUT /api/me/password HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"current_password": "Password123!",
|
||||
"password": "Password123!",
|
||||
"password_confirmation": "Password123!"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
현재 로그인한 사용자가 자신의 비밀번호를 변경한다. 프론트의 비밀번호 변경 화면(`mypage/change-password.json`)이 사용한다. `current_password` 는 `current_password:sanctum` 규칙으로 검증되어 현재 비밀번호가 틀리면 실패하며, 새 비밀번호는 8자 이상이면서 `password_confirmation` 과 일치해야 한다. 해싱은 `UserService::updateUser()` 가 담당한다. 비밀번호 변경이 본인인증(IDV) 대상으로 설정된 경우 확장이 428 을 유발할 수 있으며, 이는 글로벌 예외 핸들러가 처리한다.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,231 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.user.profile.show -->
|
||||
- **라우트명**: `api.user.profile.show`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@show`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.profile.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/user/profile HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`core.profile.read`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
`GET /api/me` 와 동일하게 `ProfileController@show` 를 호출해 현재 사용자의 프로필을 조회하지만, `permission:core.profile.read` 권한 미들웨어가 추가된 경로다. 응답 형태는 `UserResource::toProfileArray()` 산물로 `GET /api/me` 와 같으며, 필드별 소유(확장 병합) 규칙도 동일하다. 실측 예시가 비어 있는 것은 문서 생성 시 샘플 사용자가 해당 권한을 갖지 못해 403 이 반환되었기 때문이며, 응답 필드는 `GET /api/me` 문서를 참조한다.
|
||||
|
||||
|
||||
### PUT /api/user/profile
|
||||
<!-- @generated:start:api.user.profile.update -->
|
||||
- **라우트명**: `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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PUT /api/user/profile HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "예시 이름",
|
||||
"nickname": "예시 이름",
|
||||
"email": "user@example.com",
|
||||
"password": "Password123!",
|
||||
"current_password": "Password123!",
|
||||
"language": "ko",
|
||||
"country": "KR",
|
||||
"timezone": "Asia/Seoul",
|
||||
"homepage": "https://example.com",
|
||||
"mobile": "010-1234-5678",
|
||||
"phone": "010-1234-5678",
|
||||
"zipcode": "06234",
|
||||
"address": "서울특별시 강남구 테헤란로 1",
|
||||
"address_detail": "서울특별시 강남구 테헤란로 1",
|
||||
"signature": "예시값",
|
||||
"bio": "예시 내용입니다.",
|
||||
"notify_post_complete": true,
|
||||
"notify_post_reply": true,
|
||||
"notify_comment": true,
|
||||
"notify_reply_comment": true
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`core.profile.update`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
`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
|
||||
<!-- @generated:start:api.user.profile.activity-log -->
|
||||
- **라우트명**: `api.user.profile.activity-log`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@activityLog`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) + `permission:core.profile.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/user/profile/activity-log HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`core.profile.read`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
현재 사용자의 최근 활동 로그를 조회한다(`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
|
||||
<!-- @generated:start:api.user.profile.update-language -->
|
||||
- **라우트명**: `api.user.profile.update-language`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Auth\ProfileController@updateLanguage`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/user/profile/update-language HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 400 | Bad Request | `language` 값이 `config('app.supported_locales')`(기본 `['ko','en']`)에 포함되지 않는 미지원 로케일인 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
현재 사용자의 언어 설정만 변경한다. 요청 본문의 `language` 값을 받아 `config('app.supported_locales')`(기본 `['ko','en']`)에 포함되는지 검사하며, 허용되지 않는 값이면 400 을 반환한다. 프로필 전체 수정 없이 언어만 즉시 전환할 때 사용하며, 성공 시 갱신된 사용자 정보가 `UserResource` 형태로 반환된다. `language` 는 FormRequest 가 아닌 컨트롤러에서 직접 읽어 검증하므로 문서 상단 파라미터 표에는 자동 수집되지 않는다.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
# 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
|
||||
<!-- @generated:start: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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/search?q=%EC%98%88%EC%8B%9C%EA%B0%92&type=%EC%98%88%EC%8B%9C%EA%B0%92&sort=relevance&page=1&per_page=1&board_slug=example-key&category_id=1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| q | string | `` | 실제 검색에 사용된 검색어 (요청 `q` 를 trim 하여 에코, 검색어가 비어 있으면 빈 문자열) |
|
||||
| total | integer | `0` | 전체 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "검색어를 입력해주세요.",
|
||||
"data": {
|
||||
"q": "",
|
||||
"total": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
프론트엔드 통합 검색(`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`를 참고하세요.
|
||||
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.admin.seo.cached-urls -->
|
||||
- **라우트명**: `api.admin.seo.cached-urls`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\SeoCacheController@cachedUrls`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.settings.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/seo/cached-urls HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| urls | array | `[]` | 현재 사전 렌더 캐시에 남아 있는 봇 대상 페이지 URL 목록 (SeoCacheManager 인덱스에서 조회). |
|
||||
| count | integer | `0` | 캐시된 URL 개수 (`urls` 배열 길이). |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "messages.success",
|
||||
"data": {
|
||||
"urls": [],
|
||||
"count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
SeoCacheManager 인덱스에서 현재 캐시된 SEO 페이지 URL 목록과 개수를 조회합니다. 어떤 봇 대상 페이지가 사전 렌더 캐시로 남아 있는지 확인하는 진단 용도로 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/seo/clear-cache
|
||||
<!-- @generated:start: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 | 아니오 | — | 모듈 식별자 필터. 검증 규칙에는 정의되어 있으나 현재 컨트롤러 로직에서는 사용되지 않는다(향후 모듈 단위 캐시 무효화 확장 예약 필드). |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/seo/clear-cache HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"layout": "예시값",
|
||||
"module": "예시값"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
SEO 사전 렌더 캐시를 삭제합니다. `layout` 을 지정하면 해당 레이아웃 캐시만 무효화하고, 지정하지 않으면 전체 SEO 캐시를 삭제합니다. 응답의 `data.cleared` 는 `layout` 지정 시 무효화된 항목 수(정수), 미지정 시 문자열 `"all"` 입니다. 설정이나 콘텐츠 변경 후 오래된 봇 응답이 캐시로 남는 것을 방지할 때 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/seo/sitemap/regenerate
|
||||
<!-- @generated:start:api.admin.seo.sitemap.regenerate -->
|
||||
- **라우트명**: `api.admin.seo.sitemap.regenerate`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\SeoCacheController@regenerateSitemap`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.settings.update`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/seo/sitemap/regenerate HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| last_updated_at | string | `2026-07-08T03:14:49+00:00` | last updated 일시 |
|
||||
| size_bytes | integer | `25489` | 크기 (바이트) |
|
||||
| ttl | integer | `86400` | 캐시 유효 시간 (Time To Live, 초) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Sitemap 재생성이 완료되었습니다.",
|
||||
"data": {
|
||||
"last_updated_at": "2026-07-08T03:14:49+00:00",
|
||||
"size_bytes": 25489,
|
||||
"ttl": 86400
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
sitemap.xml 을 즉시 재생성합니다. SitemapManager 에 위임하며 큐 드라이버와 무관하게 동기(즉시) 실행되고, 완료 후 마지막 생성 시각을 갱신합니다. SEO 설정에서 sitemap 기능이 비활성인 경우 400(`seo.sitemap_disabled`), 생성 실패 시 500 을 반환합니다. 관리자가 콘텐츠 변경 후 검색엔진에 노출할 sitemap 을 스케줄 대기 없이 즉시 갱신할 때 사용합니다.
|
||||
|
||||
|
||||
### GET /api/admin/seo/stats
|
||||
<!-- @generated:start:api.admin.seo.stats -->
|
||||
- **라우트명**: `api.admin.seo.stats`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\SeoCacheController@stats`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.settings.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/seo/stats HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`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 페이지가 캐시로 재사용되는지 파악하는 용도. |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "messages.success",
|
||||
"data": {
|
||||
"overall": {
|
||||
"total_entries": 0,
|
||||
"hits": 0,
|
||||
"misses": 0,
|
||||
"hit_rate": 0,
|
||||
"avg_response_time_ms": null
|
||||
},
|
||||
"by_layout": [],
|
||||
"by_module": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
SEO 캐시 적중 현황을 최근 7일 기준으로 전체·레이아웃별·모듈별로 조회합니다. 봇 대상 사전 렌더 캐시가 얼마나 효과적으로 재사용되는지(적중률, 렌더 비용 절감)를 모니터링하는 관리자 대시보드용 통계입니다.
|
||||
|
||||
|
||||
### POST /api/admin/seo/warmup
|
||||
<!-- @generated:start:api.admin.seo.warmup -->
|
||||
- **라우트명**: `api.admin.seo.warmup`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Admin\SeoCacheController@warmup`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:core.settings.update`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/seo/warmup HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
SEO 캐시 워밍업(모든 SEO 레이아웃 사전 렌더)을 위한 엔드포인트입니다. 현재 컨트롤러는 실제 워밍업 로직 없이 `status: dispatched` 와 안내 메시지만 반환합니다(실 렌더링은 후속 구현 예정). 응답 성공은 요청 접수만을 의미하며 이 시점에 캐시가 채워지지는 않습니다.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
# 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
|
||||
<!-- @generated:start: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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/me/verify-password HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"password": "Password123!"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-401 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
현재 로그인한 사용자의 비밀번호를 재확인한다. 민감한 작업 직전 본인 확인 게이트로 사용되며, 프론트의 `_password_verify_section.json` 이 호출한다. 요청의 `password` 를 `Hash::check` 로 저장된 해시와 대조해, 일치하면 성공 응답(`user.password_verified`)을, 틀리면 401(`user.password_incorrect`)을 반환한다. 비밀번호를 변경하지 않고 신원만 확인하므로 사용자 데이터는 바뀌지 않는다.
|
||||
|
||||
|
||||
+23
-16
@@ -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');
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -334,11 +334,14 @@ protected function loadModuleRoutes(): void
|
||||
|
||||
### 동작 표
|
||||
|
||||
| 환경 | `installer_completed` | 동작 |
|
||||
|------|----------------------|------|
|
||||
| 프로덕션 (설치 완료) | `true` | 가드 통과 → `hasTable` 스킵 (쿼리 0건) |
|
||||
| 인스톨러 실행 중 / 마이그레이션 전 | `false` (기본값) | 기존 `hasTable` 폴백 경로 (원본 동작 보존) |
|
||||
| 테스트 (`.env.testing` 에 플래그 없음) | `false` | 기존 `hasTable` 경로 |
|
||||
| 환경 | `installer_completed` | 마이그레이션 명령 | 동작 |
|
||||
|------|----------------------|------------------|------|
|
||||
| 프로덕션 (설치 완료, 웹 요청) | `true` | 아님 | 가드 통과 → `hasTable` 스킵 (쿼리 0건) |
|
||||
| 설치 완료 `.env` 복사 + 빈 DB (마이그레이션 전) | `true` | **실행 중** | fast-path 무력화 → `hasTable` 폴백 (table not found 부팅 실패 방지) |
|
||||
| 인스톨러 실행 중 / 마이그레이션 전 | `false` (기본값) | — | 기존 `hasTable` 폴백 경로 (원본 동작 보존) |
|
||||
| 테스트 (`.env.testing` 에 플래그 없음) | `false` | — | 기존 `hasTable` 경로 |
|
||||
|
||||
두 번째 행이 이 가드의 핵심 케이스다. `INSTALLER_COMPLETED=true` 가 적힌 `.env` 를 빈 DB 새 서버에 복사한 뒤 `php artisan migrate` 로 테이블을 만들기 **전** 에 앱이 부팅되면, fast-path 가 테이블 존재를 잘못 전제하여 뒤따르는 쿼리가 "table not found" 로 부팅을 깨뜨린다. 이 컨텍스트에서는 fast-path 를 신뢰하지 않고 실제 `hasTable` 검증 경로로 폴백해야 한다 (아래 "마이그레이션 컨텍스트 가드" 참조).
|
||||
|
||||
### 적용 가이드
|
||||
|
||||
@@ -346,6 +349,66 @@ protected function loadModuleRoutes(): void
|
||||
- **`hasTable` 폴백 경로는 반드시 유지** — `installer_completed=false` 환경에서도 안전하게 부팅되어야 함
|
||||
- **try/catch 도 그대로 유지** — DB 연결 실패 시 안전한 스킵 계약 준수
|
||||
- 가드는 hasTable 체크 **전체 블록을 `if (! config('app.installer_completed'))` 로 래핑** 하는 형태
|
||||
- **fast-path 이후 쿼리가 try/catch 로 보호되지 않는 지점은 마이그레이션 컨텍스트 가드를 함께 적용** (아래 참조). 예외로 보호되는 지점은 빈 DB 에서도 안전하므로 불필요
|
||||
|
||||
### 마이그레이션 컨텍스트 가드
|
||||
|
||||
`installer_completed` fast-path 는 테이블 존재를 전제한다. 그러나 `INSTALLER_COMPLETED=true` 가 적힌 `.env` 를 빈 DB 새 서버에 복사한 뒤 `php artisan migrate` 로 테이블을 만들기 **전** 에 앱이 부팅되면, fast-path 가 `true` 를 반환하고 곧이어 실제 쿼리(`Model::where()->pluck()` 등)가 실행되어 "table not found" 로 부팅이 실패한다. 마이그레이션을 실행하려는 부팅에서 그 명령이 오히려 실패하는 닭-달걀 상태다.
|
||||
|
||||
이를 막기 위해, 스키마를 파괴/재생성하는 마이그레이션 계열 콘솔 명령 판정을 `App\Support\InstallerContext::isSchemaMutatingCommand()` 로 단일 SSoT 화하고, fast-path 지점에 가드로 결합한다. 명령 리스트: `migrate`, `migrate:fresh`, `migrate:refresh`, `migrate:rollback`, `migrate:reset`, `db:wipe`.
|
||||
|
||||
```php
|
||||
// App\Support\InstallerContext
|
||||
public static function isSchemaMutatingCommand(): bool
|
||||
{
|
||||
if (! app()->runningInConsole()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array(
|
||||
$_SERVER['argv'][1] ?? null,
|
||||
['migrate', 'migrate:fresh', 'migrate:refresh', 'migrate:rollback', 'migrate:reset', 'db:wipe'],
|
||||
true
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
가드 결합 방식은 지점의 **논리 극성** 에 따라 다르다:
|
||||
|
||||
- **Trait (fast-path 통과 조건)** — `&&` 로 결합. 마이그레이션 중이면 fast-path 를 건너뛰고 hasTable 폴백으로 진입.
|
||||
|
||||
```php
|
||||
if (! InstallerContext::isSchemaMutatingCommand() && config('app.installer_completed')) {
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
- **RouteServiceProvider (hasTable 검증 블록 진입 조건)** — `||` 로 결합. 마이그레이션 중이면 hasTable 검증 경로로 진입해 빈 DB 시 early return → 무방비 `pluck` 도달 차단.
|
||||
|
||||
```php
|
||||
if (! config('app.installer_completed') || InstallerContext::isSchemaMutatingCommand()) {
|
||||
try {
|
||||
if (! Schema::hasTable('modules')) {
|
||||
return;
|
||||
}
|
||||
// ...
|
||||
} catch (\Exception) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`isRegistryReady()` 처럼 fast-path 앞에서 조기 반환하는 지점은 `if (InstallerContext::isSchemaMutatingCommand()) { return false; }` 로 단순 게이트한다.
|
||||
|
||||
#### 부류 A (가드 필수) vs 부류 B (불필요)
|
||||
|
||||
| 부류 | 조건 | 예 | 조치 |
|
||||
|------|------|-----|------|
|
||||
| A | fast-path 이후 쿼리가 try/catch 로 **미보호** → 빈 DB 에서 부팅 실패 | `Caches{Module,Plugin,Template}Status`, `Module/PluginRouteServiceProvider` | 마이그레이션 컨텍스트 가드 적용 |
|
||||
| B | fast-path 이후 쿼리가 try/catch(\Throwable) 로 **보호** → 예외를 삼켜 안전한 기본값 반환 | `NotificationHookListener`, `IdentityPolicyRepository::{listHookTargets,activeExtensionIdentifiers}` | 가드 불필요 (예외 흡수로 안전) |
|
||||
|
||||
### config 캐시 재생성 주의
|
||||
|
||||
- `config:cache` 는 config 소스를 변경하는 라이프사이클에서 `App\Support\ConfigCacheHelper::rebuild()` 로 자동 재생성된다 (아래 "config 캐시 자동 재생성" 참조). `.env` 를 직접 편집한 경우처럼 헬퍼를 거치지 않는 변경은 여전히 `php artisan config:clear && php artisan config:cache` 를 수동 실행한다
|
||||
|
||||
### 확장 Trait 패턴
|
||||
|
||||
@@ -253,6 +253,22 @@ php artisan seo:generate-sitemap # Sitemap 생성 (큐 디스패치)
|
||||
php artisan seo:generate-sitemap --sync # Sitemap 동기 생성
|
||||
```
|
||||
|
||||
### API 문서 Artisan 커맨드
|
||||
|
||||
```bash
|
||||
# API 레퍼런스 문서 생성/갱신 (실측 기반 스캐폴딩). 상세: docs/backend/api-documentation.md
|
||||
php artisan api:docgen --scope=core # 코어 문서 생성 (범위: core|module:{id}|plugin:{id}|all)
|
||||
php artisan api:docgen --scope=core --seed # 실측용 완전 샘플 시드 후 생성 (개발 환경 전용)
|
||||
php artisan api:docgen --scope=core --base-url=https://example.com # 실측 기준 URL 지정
|
||||
php artisan api:docgen --scope=core --examples-only # 표·서술 불가침, 요청/응답 예시 블록만 in-place 삽입
|
||||
php artisan api:docgen --scope=core --check # 생성 없이 누락/drift 만 리포트 (하네스 소비)
|
||||
php artisan api:docgen --scope=core --dry-run # 생성 대상 파일/엔드포인트 목록만 출력
|
||||
|
||||
# 파라미터/응답 필드 설명 TODO 셀 in-place 백필 (재생성 없이 공통 필드 자동 서술, 멱등)
|
||||
php artisan api:docgen-backfill-params # 요청 파라미터 표 TODO 셀 백필
|
||||
php artisan api:docgen-backfill-fields # 응답 필드 표 TODO 셀 백필
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 확장 업데이트 (CLI + API)
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. URL prefix 자동: /api/admin/[vendor-module]/...
|
||||
2. name prefix 자동: api.[vendor-module]....
|
||||
1. URL prefix 자동: /api/modules/[vendor-module]/...
|
||||
2. name prefix 자동: api.modules.[vendor-module]....
|
||||
3. 활성화된 모듈만 라우트 등록됨
|
||||
4. 프론트엔드: routes/admin.json, routes/user.json으로 admin/user 분기
|
||||
5. 레거시 routes.json은 admin으로 폴백 (경고 로그 출력)
|
||||
|
||||
@@ -185,6 +185,40 @@ G7이 자동으로 주입하는 `_global` 속성입니다. 레이아웃에서
|
||||
- 상세 설명: [`docs/extension/plugin-development.md`](../extension/plugin-development.md) "폼 상태 정합성" 섹션
|
||||
- 구현 참고: [`DynamicRenderer.tsx`](../../resources/js/core/template-engine/DynamicRenderer.tsx) `performStateUpdate` 상단 주석 (~50줄)
|
||||
|
||||
### `_localInit` 은 단일 슬롯이 아니다 (engine-v1.52.2+)
|
||||
|
||||
데이터소스의 `initLocal` 이 만든 초기화 payload 는 `dataContext._localInit` 한 키로 전달된다.
|
||||
생산부는 `updateTemplateData`(3개 write site), 소비부는 `DynamicRenderer` 의 `_localInit` useEffect 다.
|
||||
|
||||
소비는 React commit **이후**에 일어난다. progressive 데이터소스는 응답이 오는 대로 각자 독립적으로
|
||||
`updateTemplateData({ _localInit })` 를 호출하므로, `initLocal` 을 가진 progressive 소스가 둘 이상이면
|
||||
두 호출이 같은 commit 사이에 들어올 수 있다. 이때 슬롯을 교체하면 먼저 도착한 payload 가
|
||||
한 번도 관측되지 않고 사라진다.
|
||||
|
||||
`_localInit` 슬롯 병합과 `__g7LocalInitTracking` 레지스트리의 단일 소유자는
|
||||
[`localInitSlot.ts`](../../resources/js/core/template-engine/localInitSlot.ts) 다.
|
||||
|
||||
```text
|
||||
❌ updateTemplateData 에서 _localInit 을 얕은 스프레드(...data)로 교체
|
||||
❌ 소비 여부와 무관하게 _localInit 을 무조건 누적 병합
|
||||
(소비가 끝난 payload 가 refetchDataSource 시 재적용되어 사용자 폼 편집을 되돌린다)
|
||||
❌ 소비 여부 판정을 위해 해시 계산식을 생산부에 복제
|
||||
(생산·소비 양쪽의 판정 기준이 어긋난다 — 슬롯 참조 비교를 쓴다)
|
||||
❌ __g7LocalInitTracking 을 레이아웃 전환 시 리셋하지 않음
|
||||
(_local 은 비웠는데 추적 해시가 남아 새 레이아웃의 동일 payload 가 "이미 적용됨" 으로 건너뛰어진다)
|
||||
```
|
||||
|
||||
```text
|
||||
✅ 아직 관측되지 않은(unconsumed) 슬롯만 누적 병합 — mergeLocalInitSlot()
|
||||
✅ 관측 시 슬롯 참조 기록 — markLocalInitConsumed(). 적용/건너뜀 여부와 무관하게 호출
|
||||
✅ 레이아웃 전환으로 _local 을 리셋할 때 추적 레지스트리도 함께 초기화 — resetLocalInitTracking()
|
||||
✅ _forceLocalInit 은 한쪽에만 있으면 보존, 양쪽에 있으면 최신 타임스탬프 (refetchOnMount 강제 초기화 유실 방지)
|
||||
✅ 소비부는 A(setLocalDynamicState)/B(_globalSetState) 양쪽을 갱신 — 이 대칭을 깨지 않는다
|
||||
```
|
||||
|
||||
> 이 결함은 정상 CPU 에서 간헐적이라 놓치기 쉽다. CPU 스로틀링(6배 이상)으로 commit 을 지연시켜야
|
||||
> 경합 창이 결정적으로 열린다.
|
||||
|
||||
### 라이프사이클 (SPA 네비게이션)
|
||||
|
||||
SPA navigate 시 `_local` 상태는 **레이아웃 이름 기준으로 선택적 초기화**됩니다:
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.2] - 2026-07-10
|
||||
|
||||
### Removed
|
||||
|
||||
- 배송설정의 배송가능 국가 추가 화면이 설치된 언어 전체에 대해 국가명 입력칸을 자동으로 만들도록 바뀌면서, 더 이상 쓰이지 않게 된 "한국어명"·"영문명" 라벨과 입력 예시 문구를 제거했습니다.
|
||||
|
||||
## [1.0.1] - 2026-07-08
|
||||
|
||||
### Added
|
||||
|
||||
-4
@@ -280,10 +280,6 @@
|
||||
"country_code": "国コード",
|
||||
"country_code_placeholder": "例:KR",
|
||||
"country_name": "国名",
|
||||
"country_name_ko": "日本語名",
|
||||
"name_ko_placeholder": "例:日本",
|
||||
"country_name_en": "英文名",
|
||||
"name_en_placeholder": "例:Japan",
|
||||
"is_active": "使用",
|
||||
"actions": "管理",
|
||||
"delete": "削除",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"en": "G7 module (sirsoft-ecommerce) Japanese language pack (bundled)",
|
||||
"ja": "G7 モジュール (sirsoft-ecommerce) 日本語 言語パック(バンドル)"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"scope": "module",
|
||||
"target_identifier": "sirsoft-ecommerce",
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- 주문 설정 화면의 KG 이니시스 테스트모드 경고(`kginicis_test_mode_status`, `test_mode_settings_warning_*`) 일본어 번역 추가 — 테스트결제 상태 안내와 실결제 설정 이동 버튼이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
|
||||
## [1.0.0] - 2026-07-01
|
||||
|
||||
### Added
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"kginicis_cbt_reconciliation": "海外決済精算",
|
||||
"kginicis_escrow_delivery": "エスクロー配送",
|
||||
"kginicis_status": "決済ステータス",
|
||||
"kginicis_test_mode_status": "テストモード状態",
|
||||
"kginicis_test_map": "テスト決済マップ",
|
||||
"settings": "決済設定",
|
||||
"vbank_info": "仮想口座情報"
|
||||
@@ -74,6 +75,10 @@
|
||||
"test_mode_badge": "テスト決済",
|
||||
"test_mode_warning_badge": "テスト決済注意事項",
|
||||
"test_mode_order_notice": "この注文はテスト決済として処理されました。実際のカード承認・出金通知が発生する可能性がありますが、KGイニシスのテスト取引は毎日23:00~23:50の間に自動キャンセルされる可能性があります。絶対に配送しないでください。",
|
||||
"test_mode_settings_warning_plugin": "KGイニシス",
|
||||
"test_mode_settings_warning_title": "(注意)KGイニシス決済設定が現在テスト決済になっています。",
|
||||
"test_mode_settings_warning_body": "テスト決済では実際の決済は発生しません。ショップ運営中の場合は必ず実決済に設定して運営してください。",
|
||||
"test_mode_settings_warning_action": "実決済に設定",
|
||||
"order_verify_title": "KGイニシス決済検証",
|
||||
"order_verify_description": "KGイニシスサーバーからリアルタイムで決済ステータスを照会します。",
|
||||
"order_verify_button": "決済照会",
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.2] - 2026-07-10
|
||||
|
||||
### Added
|
||||
|
||||
- 레이아웃 편집기의 페이지 상태 선택 항목(기본 / 모바일 메뉴 펼침) 일본어 번역 추가 — 관리자 베이스 레이아웃을 편집할 때 모바일 메뉴가 펼쳐진 상태를 미리 보며 편집할 수 있고, 그 선택지가 일본어 로케일에서도 올바르게 표시됩니다.
|
||||
|
||||
## [1.0.1] - 2026-07-08
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1393,7 +1393,9 @@
|
||||
"page_detail_default": "基本",
|
||||
"page_detail_attachments_open": "添付を展開",
|
||||
"order_detail_default": "通常注文",
|
||||
"order_detail_partially_cancelled": "部分キャンセル"
|
||||
"order_detail_partially_cancelled": "部分キャンセル",
|
||||
"admin_base_default": "基本",
|
||||
"sidebar_open": "モバイルメニュー展開"
|
||||
},
|
||||
"cell_border": {
|
||||
"side_all": "すべて",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"en": "G7 template (sirsoft-admin_basic) Japanese language pack (bundled)",
|
||||
"ja": "G7 テンプレート (sirsoft-admin_basic) 日本語 言語パック(バンドル)"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"scope": "template",
|
||||
"target_identifier": "sirsoft-admin_basic",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# API 레퍼런스 문서 목차
|
||||
|
||||
> **소유**: 모듈 `gnuboard7-hello_module` · **생성**: `php artisan api:docgen` (실측 기반).
|
||||
> 아래 표는 자동 생성됩니다. 각 문서를 열면 엔드포인트별 파라미터·응답·예시를 볼 수 있습니다.
|
||||
|
||||
<!-- @generated:start:api-readme-index -->
|
||||
- **문서 수**: 1 · **엔드포인트 수**: 2
|
||||
|
||||
| 문서 | 도메인 | 엔드포인트 |
|
||||
| --- | --- | --- |
|
||||
| [memos.md](memos.md) | `memos` | 2 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.modules.gnuboard7-hello_module.memos.index -->
|
||||
- **라우트명**: `api.modules.gnuboard7-hello_module.memos.index`
|
||||
- **컨트롤러**: `Modules\Gnuboard7\HelloModule\Http\Controllers\Api\MemoController@index`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/gnuboard7-hello_module/memos HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
메모 목록을 페이지네이션으로 조회하는 공개 엔드포인트다. 라우트는 `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}
|
||||
<!-- @generated:start:api.modules.gnuboard7-hello_module.memos.show -->
|
||||
- **라우트명**: `api.modules.gnuboard7-hello_module.memos.show`
|
||||
- **컨트롤러**: `Modules\Gnuboard7\HelloModule\Http\Controllers\Api\MemoController@show`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| id | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/gnuboard7-hello_module/memos/{id} HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명**
|
||||
|
||||
단일 메모를 조회하는 공개 엔드포인트다. 목록과 마찬가지로 `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
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# API 레퍼런스 문서 목차
|
||||
|
||||
> **소유**: 모듈 `sirsoft-board` · **생성**: `php artisan api:docgen` (실측 기반).
|
||||
> 아래 표는 자동 생성됩니다. 각 문서를 열면 엔드포인트별 파라미터·응답·예시를 볼 수 있습니다.
|
||||
|
||||
<!-- @generated:start:api-readme-index -->
|
||||
- **문서 수**: 10 · **엔드포인트 수**: 80
|
||||
|
||||
| 문서 | 도메인 | 엔드포인트 |
|
||||
| --- | --- | --- |
|
||||
| [activity-stats.md](activity-stats.md) | `activity-stats` | 1 |
|
||||
| [board.md](board.md) | `board` | 18 |
|
||||
| [board-activities.md](board-activities.md) | `board-activities` | 1 |
|
||||
| [board-types.md](board-types.md) | `board-types` | 4 |
|
||||
| [boards.md](boards.md) | `boards` | 37 |
|
||||
| [dashboard.md](dashboard.md) | `dashboard` | 4 |
|
||||
| [my-comments.md](my-comments.md) | `my-comments` | 1 |
|
||||
| [reports.md](reports.md) | `reports` | 7 |
|
||||
| [settings.md](settings.md) | `settings` | 5 |
|
||||
| [users.md](users.md) | `users` | 2 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.modules.sirsoft-board.me.activity-stats -->
|
||||
- **라우트명**: `api.modules.sirsoft-board.me.activity-stats`
|
||||
- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\UserActivityController@stats`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/me/activity-stats HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| total_posts | integer | `1` | 회원 본인이 작성한 게시글 총수입니다. 비활성 게시판의 글은 제외하며 소프트 삭제된 글도 집계에서 빠집니다. |
|
||||
| total_comments | integer | `0` | 회원 본인이 작성한 게시글들의 댓글 수 합계(SUM of comments_count)입니다. 비활성 게시판 글은 제외됩니다. |
|
||||
| total_views | integer | `42` | 회원 본인이 작성한 게시글들의 누적 조회수 합계(SUM of view_count)입니다. 비활성 게시판 글은 제외됩니다. |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "활동 통계를 조회했습니다.",
|
||||
"data": {
|
||||
"total_posts": 1,
|
||||
"total_comments": 0,
|
||||
"total_views": 44
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 로그인한 회원 본인의 게시판 활동 통계(작성 글 수, 작성 댓글 수, 누적 조회수)를 마이페이지 요약 카드에 표시하기 위해 반환합니다. `auth:sanctum` 인증이 필요한 회원 전용 엔드포인트로, 대상은 항상 인증된 본인(`Auth::id()`)입니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.modules.sirsoft-board.me.board-activities.index -->
|
||||
- **라우트명**: `api.modules.sirsoft-board.me.board-activities.index`
|
||||
- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\UserActivityController@index`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/me/board-activities HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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 모드 글은 태그를 제거한 평문으로 변환되며, 목록 미리보기용으로 사용합니다. |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "내 활동 게시글을 조회했습니다.",
|
||||
"data": {
|
||||
"current_page": 1,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"board_slug": "apidoc-sample-board",
|
||||
"board_name": "API 문서 샘플 게시판",
|
||||
"activity_type": "authored",
|
||||
"activity_count": 0,
|
||||
"title": "API 문서 샘플 게시글",
|
||||
"is_secret": "{MASKED}",
|
||||
"status": "published",
|
||||
"view_count": 44,
|
||||
"comment_count": 0,
|
||||
"created_at": "2026-07-08 10:41:34",
|
||||
"created_at_formatted": "4시간 전",
|
||||
"content_plain": "API 레퍼런스 실측용 완전 샘플 게시글 본문입니다."
|
||||
}
|
||||
],
|
||||
"first_page_url": "https://api.example.com/api/modules/sirsoft-board/me/board-activities?page=1",
|
||||
"from": 1,
|
||||
"last_page": 1,
|
||||
"last_page_url": "https://api.example.com/api/modules/sirsoft-board/me/board-activities?page=1",
|
||||
"links": [
|
||||
{
|
||||
"url": null,
|
||||
"label": "pagination.previous",
|
||||
"page": null,
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"url": "https://api.example.com/api/modules/sirsoft-board/me/board-activities?page=1",
|
||||
"label": "1",
|
||||
"page": 1,
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"url": null,
|
||||
"label": "pagination.next",
|
||||
"page": null,
|
||||
"active": false
|
||||
}
|
||||
],
|
||||
"next_page_url": null,
|
||||
"path": "https://api.example.com/api/modules/sirsoft-board/me/board-activities",
|
||||
"per_page": 25,
|
||||
"prev_page_url": null,
|
||||
"to": 1,
|
||||
"total": 1,
|
||||
"query": {
|
||||
"board_slug": "",
|
||||
"search": "",
|
||||
"sort": "latest",
|
||||
"activity_type": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 로그인한 회원 본인의 게시글 활동을 마이페이지에 표시하기 위해 반환합니다. `auth:sanctum` 인증이 필요한 회원 전용 엔드포인트로, 대상 사용자는 항상 인증된 본인(`Auth::id()`)입니다. `board_slug`·`search`·`activity_type`·`sort`(latest/oldest/views) 필터와 `per_page`(기본 20) 페이지네이션을 지원하며, 응답에는 적용된 필터가 `query` 로 함께 담깁니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.modules.sirsoft-board.admin.board-types.index -->
|
||||
- **라우트명**: `api.modules.sirsoft-board.admin.board-types.index`
|
||||
- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardTypeController@index`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.boards.create`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/admin/board-types HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| slug | string | `basic` | 유형 식별자 (basic, card, gallery 등) |
|
||||
| name | object | `{"ko":"기본형","en":"Basic List","ja":"基本形"}` | 유형명 (다국어: {"ko": "기본형", "en": "Basic List"}) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "게시판 유형 목록을 조회했습니다.",
|
||||
"data": [
|
||||
{
|
||||
"id": 2,
|
||||
"slug": "basic",
|
||||
"name": {
|
||||
"ko": "기본형",
|
||||
"en": "Basic List"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"slug": "gallery",
|
||||
"name": {
|
||||
"ko": "갤러리형",
|
||||
"en": "Gallery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"slug": "card",
|
||||
"name": {
|
||||
"ko": "카드형",
|
||||
"en": "Card"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.create`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 관리자가 게시판 생성/편집 화면에서 선택할 수 있는 게시판 유형(basic, card, gallery 등) 목록을 반환합니다. `auth:sanctum` 인증과 `sirsoft-board.boards.create` 권한이 필요하며, 게시판 생성 권한을 재사용해 접근을 통제합니다. `name` 은 다국어 객체로 반환되므로 표시 시 현재 로케일 키를 선택해야 합니다.
|
||||
|
||||
|
||||
### POST /api/modules/sirsoft-board/admin/board-types
|
||||
<!-- @generated:start:api.modules.sirsoft-board.admin.board-types.store -->
|
||||
- **라우트명**: `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 | 예 | — | 대상의 이름/명칭 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/modules/sirsoft-board/admin/board-types HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"slug": "example-key",
|
||||
"name": "예시 이름"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.create`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 새 게시판 유형을 생성합니다. `slug` 는 유형을 식별하는 고유 문자열(최대 50자)이고 `name` 은 유형명입니다. `auth:sanctum` 인증과 `sirsoft-board.boards.create` 권한이 필요하며, 성공 시 생성된 유형 리소스와 함께 201 을 반환합니다.
|
||||
|
||||
|
||||
### DELETE /api/modules/sirsoft-board/admin/board-types/{id}
|
||||
<!-- @generated:start:api.modules.sirsoft-board.admin.board-types.destroy -->
|
||||
- **라우트명**: `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 | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/modules/sirsoft-board/admin/board-types/2 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.create`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 지정한 `id` 의 게시판 유형을 삭제합니다. `auth:sanctum` 인증과 `sirsoft-board.boards.create` 권한이 필요합니다. 존재하지 않는 id 는 404, 해당 유형을 사용 중인 게시판이 있는 등 삭제할 수 없는 경우 422 를 반환합니다.
|
||||
|
||||
|
||||
### PUT /api/modules/sirsoft-board/admin/board-types/{id}
|
||||
<!-- @generated:start:api.modules.sirsoft-board.admin.board-types.update -->
|
||||
- **라우트명**: `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 | 예 | — | 대상의 이름/명칭 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PUT /api/modules/sirsoft-board/admin/board-types/2 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "예시 이름"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.create`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 지정한 `id` 의 게시판 유형명을 수정합니다. `slug` 는 변경되지 않으며 `name` 만 갱신합니다. `auth:sanctum` 인증과 `sirsoft-board.boards.create` 권한이 필요하고, 존재하지 않는 id 는 404 를 반환합니다.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,320 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.modules.sirsoft-board.admin.dashboard.overview -->
|
||||
- **라우트명**: `api.modules.sirsoft-board.admin.dashboard.overview`
|
||||
- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\DashboardController@overview`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/admin/dashboard/overview HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| today_posts | integer | `0` | 오늘 등록된 새 게시글 수입니다. board_stats 집계 테이블의 오늘 행에서 읽으며, 행이 없으면 0 을 반환합니다(최대 1시간 지연). |
|
||||
| today_comments | integer | `0` | 오늘 등록된 새 댓글 수입니다. board_stats 집계 테이블의 오늘 행에서 읽으며, 행이 없으면 0 을 반환합니다(최대 1시간 지연). |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "sirsoft-board::messages.dashboard.fetch_success",
|
||||
"data": {
|
||||
"today_posts": 0,
|
||||
"today_comments": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 관리자 대시보드 위젯에 표시할 오늘의 게시판 현황을 반환합니다. `auth:sanctum` 인증이 필요하며(대시보드 진입은 코어 `core.dashboard.read` 가드로 보호), 오늘 등록된 새 글 수(today_posts)와 새 댓글 수(today_comments)를 집계하여 반환합니다.
|
||||
|
||||
|
||||
### GET /api/modules/sirsoft-board/admin/dashboard/pending-reports
|
||||
<!-- @generated:start: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 | 반환할 최대 항목 수 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/admin/dashboard/pending-reports?limit=1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| items | array | `[{"id":7,"board_slug":"qna","board_name":"Q&A","target_ty…` | 전체 게시판을 가로질러 조회한 미처리 신고 항목 목록입니다. 각 항목은 신고 대상 게시판(board_slug/board_name), 대상 종류(target_type), 대상 제목/발췌(target_title/target_excerpt), 상태, 신고 시각을 포함합니다. `limit` 개까지 반환됩니다. |
|
||||
| total | integer | `16` | 전체 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "sirsoft-board::messages.dashboard.fetch_success",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"board_slug": "apidoc-sample-board",
|
||||
"board_name": "API 문서 샘플 게시판",
|
||||
"target_type": "post",
|
||||
"target_type_label": "게시글",
|
||||
"target_post_id": 1,
|
||||
"target_title": "API 문서 샘플 게시글",
|
||||
"target_excerpt": null,
|
||||
"status": "pending",
|
||||
"status_label": "접수",
|
||||
"author_name": "API 문서 샘플 사용자",
|
||||
"last_reported_at": "4시간 전"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 전체 게시판의 미처리 신고 목록과 총 건수를 대시보드 위젯에 표시하기 위해 반환합니다. `auth:sanctum` 인증이 필요하며(대시보드 진입은 코어 `core.dashboard.read` 가드로 보호), `limit`(1~50, 기본 5)으로 표시 건수를 제어합니다. 응답은 미처리 신고 항목(items)과 전체 미처리 건수(total)를 포함합니다.
|
||||
|
||||
|
||||
### GET /api/modules/sirsoft-board/admin/dashboard/post-graph
|
||||
<!-- @generated:start: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`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/admin/dashboard/post-graph HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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 설정)으로 포맷한 표시용 문자열입니다. 집계 행이 없으면 빈 문자열입니다. |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "sirsoft-board::messages.dashboard.fetch_success",
|
||||
"data": {
|
||||
"days": [
|
||||
{
|
||||
"date": "2026-07-02",
|
||||
"post_count": 0,
|
||||
"comment_count": 0
|
||||
},
|
||||
{
|
||||
"date": "2026-07-03",
|
||||
"post_count": 0,
|
||||
"comment_count": 0
|
||||
},
|
||||
{
|
||||
"date": "2026-07-04",
|
||||
"post_count": 0,
|
||||
"comment_count": 0
|
||||
},
|
||||
{
|
||||
"date": "2026-07-05",
|
||||
"post_count": 0,
|
||||
"comment_count": 0
|
||||
},
|
||||
{
|
||||
"date": "2026-07-06",
|
||||
"post_count": 0,
|
||||
"comment_count": 0
|
||||
},
|
||||
{
|
||||
"date": "2026-07-07",
|
||||
"post_count": 0,
|
||||
"comment_count": 0
|
||||
},
|
||||
{
|
||||
"date": "2026-07-08",
|
||||
"post_count": 0,
|
||||
"comment_count": 0
|
||||
}
|
||||
],
|
||||
"total_posts": 0,
|
||||
"total_comments": 0,
|
||||
"posts_change": null,
|
||||
"comments_change": null,
|
||||
"updated_at": null,
|
||||
"updated_at_display": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 대시보드 위젯의 추세 그래프에 표시할 최근 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
|
||||
<!-- @generated:start: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 | 반환할 최대 항목 수 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/admin/dashboard/recent-posts?limit=1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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시간 전` | 생성 일시 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "sirsoft-board::messages.dashboard.fetch_success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"board_slug": "apidoc-sample-board",
|
||||
"board_name": "API 문서 샘플 게시판",
|
||||
"title": "API 문서 샘플 게시글",
|
||||
"author_name": "API 문서 샘플 사용자",
|
||||
"comments_count": 0,
|
||||
"created_at": "4시간 전"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 전체 게시판에서 최신 게시글을 대시보드 위젯에 표시하기 위해 반환합니다. `auth:sanctum` 인증이 필요하며(대시보드 진입은 코어 `core.dashboard.read` 가드로 보호), `limit`(1~50, 기본 5)으로 표시 건수를 제어합니다. 각 항목은 게시판(board_slug/board_name), 제목, 작성자, 댓글 수, 작성 시각(상대 시간 표기)을 포함합니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.modules.sirsoft-board.me.my-comments.index -->
|
||||
- **라우트명**: `api.modules.sirsoft-board.me.my-comments.index`
|
||||
- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\UserActivityController@myComments`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/me/my-comments HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "내 활동 게시글을 조회했습니다.",
|
||||
"data": {
|
||||
"current_page": 1,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"board_slug": "apidoc-sample-board",
|
||||
"board_name": "API 문서 샘플 게시판",
|
||||
"post_title": "API 문서 샘플 게시글",
|
||||
"post_id_val": 1,
|
||||
"content": "API 문서 샘플 댓글입니다.",
|
||||
"created_at": "2026-07-08 10:41:34",
|
||||
"created_at_formatted": "4시간 전"
|
||||
}
|
||||
],
|
||||
"first_page_url": "https://api.example.com/api/modules/sirsoft-board/me/my-comments?page=1",
|
||||
"from": 1,
|
||||
"last_page": 1,
|
||||
"last_page_url": "https://api.example.com/api/modules/sirsoft-board/me/my-comments?page=1",
|
||||
"links": [
|
||||
{
|
||||
"url": null,
|
||||
"label": "pagination.previous",
|
||||
"page": null,
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"url": "https://api.example.com/api/modules/sirsoft-board/me/my-comments?page=1",
|
||||
"label": "1",
|
||||
"page": 1,
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"url": null,
|
||||
"label": "pagination.next",
|
||||
"page": null,
|
||||
"active": false
|
||||
}
|
||||
],
|
||||
"next_page_url": null,
|
||||
"path": "https://api.example.com/api/modules/sirsoft-board/me/my-comments",
|
||||
"per_page": 25,
|
||||
"prev_page_url": null,
|
||||
"to": 1,
|
||||
"total": 1,
|
||||
"query": {
|
||||
"board_slug": "",
|
||||
"search": "",
|
||||
"sort": "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 로그인한 회원 본인이 작성한 댓글 목록을 마이페이지에 표시하기 위해 반환하며, 각 항목에 댓글 내용과 함께 원 게시글 제목·게시판 정보를 포함합니다. `auth:sanctum` 인증이 필요한 회원 전용 엔드포인트로, 대상은 항상 인증된 본인(`Auth::id()`)입니다. `board_slug`·`search`·`sort`(기본 latest) 필터와 `per_page`(1~100, 기본 20) 페이지네이션을 지원합니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,597 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.modules.sirsoft-board.admin.reports.index -->
|
||||
- **라우트명**: `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 | 아니오 | — | 페이지당 항목 수 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/admin/reports?filters=%EC%98%88%EC%8B%9C%EA%B0%92&status=%EC%98%88%EC%8B%9C%EA%B0%92&target_type=%EC%98%88%EC%8B%9C%EA%B0%92&target_status=%EC%98%88%EC%8B%9C%EA%B0%92&board_id=1&reported_at_from=%EC%98%88%EC%8B%9C%EA%B0%92&reported_at_to=%EC%98%88%EC%8B%9C%EA%B0%92&sort_by=%EC%98%88%EC%8B%9C%EA%B0%92&sort_order=asc&per_page=1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "신고 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"board_id": 1,
|
||||
"board": {
|
||||
"id": 1,
|
||||
"name": "API 문서 샘플 게시판",
|
||||
"slug": "apidoc-sample-board",
|
||||
"title": null,
|
||||
"current_status": null,
|
||||
"deleted_at": null
|
||||
},
|
||||
"target_type": "post",
|
||||
"target_type_label": "게시글",
|
||||
"target_id": 1,
|
||||
"post_id": 1,
|
||||
"content": null,
|
||||
"content_mode": "text",
|
||||
"content_preview": "",
|
||||
"author": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 사용자",
|
||||
"email": "apidoc-sample-user@example.com",
|
||||
"is_guest": false
|
||||
},
|
||||
"reporter": null,
|
||||
"reason_type": null,
|
||||
"reason_type_label": null,
|
||||
"status": "pending",
|
||||
"status_label": "접수",
|
||||
"status_variant": "warning",
|
||||
"processor": null,
|
||||
"processed_at": null,
|
||||
"metadata": null,
|
||||
"report_count": 0,
|
||||
"last_reported_at": "2026-07-08 10:41:34",
|
||||
"is_reactivated": false,
|
||||
"target_status": "published",
|
||||
"target_trigger_type": "user",
|
||||
"target_status_label": "게시중",
|
||||
"created_at": "2026-07-08 10:41:34",
|
||||
"updated_at": "2026-07-08 10:41:34",
|
||||
"is_owner": false,
|
||||
"abilities": {
|
||||
"can_view": true,
|
||||
"can_manage": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"statistics": {
|
||||
"by_status": {
|
||||
"pending": 1
|
||||
},
|
||||
"by_type": {
|
||||
"post": 1
|
||||
},
|
||||
"total": 1
|
||||
},
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"per_page": 20,
|
||||
"total": 1,
|
||||
"from": 1,
|
||||
"to": 1,
|
||||
"has_more_pages": false
|
||||
},
|
||||
"abilities": {
|
||||
"can_view": true,
|
||||
"can_manage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.view`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 신고 관리 화면의 목록을 조회합니다. `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
|
||||
<!-- @generated:start: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자). 상태 변경 사유나 조치 내용을 처리 이력에 함께 기록합니다. |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PATCH /api/modules/sirsoft-board/admin/reports/bulk-status HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ids": [
|
||||
"예시값"
|
||||
],
|
||||
"status": "예시값",
|
||||
"process_note": "예시값"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.manage`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 여러 신고의 상태를 한 번에 변경합니다. `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
|
||||
<!-- @generated:start: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` 허용값). 지정 시 해당 상태로의 일괄 전환을 가정한 상태별 건수 요약을 계산합니다. |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/modules/sirsoft-board/admin/reports/status-counts HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ids": [
|
||||
"예시값"
|
||||
],
|
||||
"target_status": "예시값"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.view`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 선택된 신고들의 상태별 건수를 집계하여 반환합니다. `auth:sanctum` 인증과 `sirsoft-board.reports.view` 권한이 필요합니다. 대량 상태 변경을 실행하기 전에 사용자에게 선택한 신고들의 상태 분포를 미리 보여주기 위한 조회용 API로, `ids` 배열(최소 1개)과 선택적 `target_status`를 받아 상태별 건수와 요약 정보를 계산합니다.
|
||||
|
||||
|
||||
### DELETE /api/modules/sirsoft-board/admin/reports/{report}
|
||||
<!-- @generated:start:api.modules.sirsoft-board.admin.reports.destroy -->
|
||||
- **라우트명**: `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의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/modules/sirsoft-board/admin/reports/1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
|
||||
|
||||
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "신고가 삭제되었습니다.",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.manage`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 신고를 삭제합니다(소프트 삭제). `auth:sanctum` 인증과 `sirsoft-board.reports.manage` 권한이 필요합니다. 경로의 `report`(신고 ID)에 해당하는 신고 케이스를 소프트 삭제하며, 존재하지 않으면 404, 스코프 권한 위반 시 403을 반환합니다.
|
||||
|
||||
|
||||
### GET /api/modules/sirsoft-board/admin/reports/{report}
|
||||
<!-- @generated:start:api.modules.sirsoft-board.admin.reports.show -->
|
||||
- **라우트명**: `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의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/admin/reports/1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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` | 최종 수정 일시 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "신고 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"board_id": 1,
|
||||
"board": {
|
||||
"id": 1,
|
||||
"name": "API 문서 샘플 게시판",
|
||||
"slug": "apidoc-sample-board"
|
||||
},
|
||||
"target_type": "post",
|
||||
"target_id": 1,
|
||||
"post": {
|
||||
"id": 1,
|
||||
"title": "API 문서 샘플 게시글",
|
||||
"content": "<p>API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.</p>",
|
||||
"content_mode": "html",
|
||||
"created_at": "2026-07-08 10:41:34",
|
||||
"author": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 사용자",
|
||||
"email": "apidoc-sample-user@example.com",
|
||||
"is_guest": false
|
||||
}
|
||||
},
|
||||
"comment": null,
|
||||
"target_status": "published",
|
||||
"target_status_label": "게시중",
|
||||
"blind_trigger_type": "user",
|
||||
"blind_trigger_type_label": "사용자 직접 삭제",
|
||||
"status": "pending",
|
||||
"available_actions": [
|
||||
"review",
|
||||
"rejected",
|
||||
"suspended",
|
||||
"deleted"
|
||||
],
|
||||
"abilities": {
|
||||
"can_view": true,
|
||||
"can_manage": true
|
||||
},
|
||||
"reporters": [],
|
||||
"report_count": 0,
|
||||
"reason_summary": "-",
|
||||
"first_reported_at": "2026-07-08 10:41:34",
|
||||
"last_reported_at": "2026-07-08 10:41:34",
|
||||
"histories": [],
|
||||
"metadata": {
|
||||
"reason": "spam"
|
||||
},
|
||||
"created_at": "2026-07-08 10:41:34",
|
||||
"updated_at": "2026-07-08 10:41:34"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.view`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 신고 케이스 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
|
||||
<!-- @generated:start:api.modules.sirsoft-board.admin.reports.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부터 시작) |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/admin/reports/1/reporters?per_page=1&page=1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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 일시 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "신고 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"data": [],
|
||||
"pagination": {
|
||||
"total": 0,
|
||||
"from": 0,
|
||||
"to": 0,
|
||||
"per_page": 25,
|
||||
"current_page": 1,
|
||||
"last_page": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.view`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 신고 케이스에 접수된 개별 신고자 목록을 페이지네이션으로 반환합니다. `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
|
||||
<!-- @generated:start:api.modules.sirsoft-board.admin.reports.update-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자). 상태 변경 사유나 조치 내용을 처리 이력에 함께 기록합니다. |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PATCH /api/modules/sirsoft-board/admin/reports/1/status HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "예시값",
|
||||
"process_note": "예시값"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.manage`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 단건 신고의 상태를 변경합니다. `auth:sanctum` 인증과 `sirsoft-board.reports.manage` 권한이 필요합니다. 경로의 `report`(신고 ID)에 대해 `status`로 지정한 상태로 전환하고 선택적으로 `process_note`(최대 1000자)를 처리 메모로 남깁니다. 영구삭제(deleted) 등 전환 불가 상태는 FormRequest 검증에서 422로 선차단되고 서비스의 deleted 가드가 2차 방어선으로 동작하며, 대상이 없으면 404를 반환합니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,821 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.modules.sirsoft-board.admin.settings.index -->
|
||||
- **라우트명**: `api.modules.sirsoft-board.admin.settings.index`
|
||||
- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\BoardSettingsController@index`
|
||||
- **인증/권한**: `auth:sanctum` + `permission:sirsoft-board.settings.read`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/admin/settings HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`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')` 기반 입력 제한값(페이지당 글 수·답글/댓글 깊이의 최소·최대 등)이 담겨 프론트 입력 검증 범위로 사용됩니다. |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "sirsoft-board::messages.settings.fetch_success",
|
||||
"data": {
|
||||
"basic_defaults": {
|
||||
"type": "basic",
|
||||
"per_page": 20,
|
||||
"per_page_mobile": 15,
|
||||
"order_by": "created_at",
|
||||
"order_direction": "DESC",
|
||||
"secret_mode": "{MASKED}",
|
||||
"use_comment": true,
|
||||
"use_reply": true,
|
||||
"max_reply_depth": 5,
|
||||
"max_comment_depth": 10,
|
||||
"comment_order": "ASC",
|
||||
"show_view_count": true,
|
||||
"use_report": false,
|
||||
"min_title_length": 2,
|
||||
"max_title_length": 200,
|
||||
"min_content_length": 2,
|
||||
"max_content_length": 10000,
|
||||
"min_comment_length": 2,
|
||||
"max_comment_length": 1000,
|
||||
"use_file_upload": false,
|
||||
"max_file_size": 10,
|
||||
"max_file_count": 5,
|
||||
"allowed_extensions": [
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"webp",
|
||||
"pdf",
|
||||
"doc",
|
||||
"docx",
|
||||
"xls",
|
||||
"xlsx",
|
||||
"ppt",
|
||||
"pptx",
|
||||
"hwp",
|
||||
"txt",
|
||||
"zip"
|
||||
],
|
||||
"notify_admin_on_post": true,
|
||||
"notify_author": true,
|
||||
"new_display_hours": 24,
|
||||
"blocked_keywords": [
|
||||
"시발",
|
||||
"씨발",
|
||||
"ㅅㅂ",
|
||||
"ㅆㅂ",
|
||||
"좆",
|
||||
"ㅈㄴ",
|
||||
"존나",
|
||||
"씹",
|
||||
"지랄",
|
||||
"ㅈㄹ",
|
||||
"개새끼",
|
||||
"새끼",
|
||||
"ㅅㄲ",
|
||||
"개자식",
|
||||
"병신",
|
||||
"ㅂㅅ",
|
||||
"미친놈",
|
||||
"미친년",
|
||||
"느금마",
|
||||
"니애미",
|
||||
"느금",
|
||||
"엠창",
|
||||
"엠뒤",
|
||||
"빡대가리",
|
||||
"개같은",
|
||||
"개년",
|
||||
"개놈",
|
||||
"개돼지",
|
||||
"개씹",
|
||||
"개쓰레기",
|
||||
"닥쳐",
|
||||
"꺼져",
|
||||
"죽어",
|
||||
"자살해",
|
||||
"뒤져",
|
||||
"저능아",
|
||||
"정신병자",
|
||||
"썅",
|
||||
"쌍놈",
|
||||
"쌍년",
|
||||
"쉬발",
|
||||
"쒸발",
|
||||
"시팔",
|
||||
"씨팔",
|
||||
"한남",
|
||||
"한녀",
|
||||
"김치녀",
|
||||
"된장녀",
|
||||
"맘충",
|
||||
"틀딱",
|
||||
"느개비",
|
||||
"장애인놈",
|
||||
"병신새끼",
|
||||
"흑형",
|
||||
"짱깨",
|
||||
"쪽바리",
|
||||
"왜놈",
|
||||
"튀기",
|
||||
"똥남아",
|
||||
"야동",
|
||||
"포르노",
|
||||
"섹스",
|
||||
"야사",
|
||||
"음란",
|
||||
"성인사이트",
|
||||
"조건만남",
|
||||
"원나잇",
|
||||
"섹파",
|
||||
"오피",
|
||||
"키스방",
|
||||
"매춘",
|
||||
"성매매",
|
||||
"몸파는"
|
||||
],
|
||||
"default_board_permissions": {
|
||||
"admin.posts.read": [
|
||||
"admin"
|
||||
],
|
||||
"admin.posts.write": [
|
||||
"admin"
|
||||
],
|
||||
"admin.posts.read-secret": [
|
||||
"admin"
|
||||
],
|
||||
"admin.comments.read": [
|
||||
"admin"
|
||||
],
|
||||
"admin.comments.write": [
|
||||
"admin"
|
||||
],
|
||||
"admin.attachments.upload": [
|
||||
"admin"
|
||||
],
|
||||
"admin.attachments.download": [
|
||||
"admin"
|
||||
],
|
||||
"admin.manage": [
|
||||
"admin"
|
||||
],
|
||||
"posts.read": [
|
||||
"admin",
|
||||
"user",
|
||||
"guest"
|
||||
],
|
||||
"posts.write": [
|
||||
"admin",
|
||||
"user"
|
||||
],
|
||||
"posts.read-secret": [
|
||||
"admin"
|
||||
],
|
||||
"comments.read": [
|
||||
"admin",
|
||||
"user",
|
||||
"guest"
|
||||
],
|
||||
"comments.write": [
|
||||
"admin",
|
||||
"user"
|
||||
],
|
||||
"attachments.upload": [
|
||||
"admin",
|
||||
"user"
|
||||
],
|
||||
"attachments.download": [
|
||||
"admin",
|
||||
"user",
|
||||
"guest"
|
||||
],
|
||||
"manager": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"report_policy": {
|
||||
"auto_hide_threshold": 5,
|
||||
"auto_hide_target": "both",
|
||||
"daily_report_limit": 10,
|
||||
"rejection_limit_count": 5,
|
||||
"rejection_limit_days": 30,
|
||||
"notify_admin_on_report": true,
|
||||
"notify_admin_on_report_scope": "per_case",
|
||||
"notify_admin_on_report_channels": [
|
||||
"mail",
|
||||
"database"
|
||||
],
|
||||
"notify_author_on_report_action": true,
|
||||
"notify_author_on_report_action_channels": [
|
||||
"mail",
|
||||
"database"
|
||||
]
|
||||
},
|
||||
"spam_security": {
|
||||
"post_cooldown_seconds": 0,
|
||||
"comment_cooldown_seconds": 0,
|
||||
"report_cooldown_seconds": 60,
|
||||
"view_count_cache_ttl": 86400
|
||||
},
|
||||
"display": {
|
||||
"date_display_format": "standard"
|
||||
},
|
||||
"seo": {
|
||||
"meta_boards_title": "{site_name}",
|
||||
"meta_boards_description": "",
|
||||
"meta_board_title": "{board_name}",
|
||||
"meta_board_description": "{board_description}",
|
||||
"meta_post_title": "{board_name} - {post_title}",
|
||||
"meta_post_description": "",
|
||||
"seo_boards": true,
|
||||
"seo_board": true,
|
||||
"seo_post_detail": true
|
||||
},
|
||||
"notifications": {
|
||||
"channels": [
|
||||
{
|
||||
"id": "mail",
|
||||
"is_active": true,
|
||||
"sort_order": 0
|
||||
},
|
||||
{
|
||||
"id": "database",
|
||||
"is_active": true,
|
||||
"sort_order": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"report_permissions": {
|
||||
"view_roles": [
|
||||
"admin",
|
||||
"apidoc-sample-role"
|
||||
],
|
||||
"manage_roles": [
|
||||
"admin",
|
||||
"apidoc-sample-role"
|
||||
]
|
||||
},
|
||||
"abilities": {
|
||||
"can_update": true
|
||||
},
|
||||
"_meta": {
|
||||
"limits": {
|
||||
"per_page_min": 5,
|
||||
"per_page_max": 100,
|
||||
"min_title_length_min": 0,
|
||||
"min_title_length_max": 200,
|
||||
"max_title_length_min": 1,
|
||||
"max_title_length_max": 1000,
|
||||
"min_content_length_min": 0,
|
||||
"min_content_length_max": 10000,
|
||||
"max_content_length_min": 1,
|
||||
"max_content_length_max": 100000,
|
||||
"min_comment_length_min": 0,
|
||||
"min_comment_length_max": 1000,
|
||||
"max_comment_length_min": 1,
|
||||
"max_comment_length_max": 10000,
|
||||
"max_file_size_min": 1,
|
||||
"max_file_size_max": 200,
|
||||
"max_file_count_min": 1,
|
||||
"max_file_count_max": 20,
|
||||
"category_max": 50,
|
||||
"max_reply_depth_min": 1,
|
||||
"max_reply_depth_max": 10,
|
||||
"max_comment_depth_min": 0,
|
||||
"max_comment_depth_max": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.settings.read`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 게시판 모듈의 전체 환경설정을 조회합니다. `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
|
||||
<!-- @generated:start:api.modules.sirsoft-board.admin.settings.store -->
|
||||
- **라우트명**: `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 활성화 여부를 저장합니다. |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PUT /api/modules/sirsoft-board/admin/settings HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"_tab": "basic_defaults",
|
||||
"notifications": [
|
||||
"예시값"
|
||||
],
|
||||
"basic_defaults": [
|
||||
"예시값"
|
||||
],
|
||||
"report_policy": [
|
||||
"예시값"
|
||||
],
|
||||
"report_permissions": [
|
||||
"예시값"
|
||||
],
|
||||
"display": [
|
||||
"예시값"
|
||||
],
|
||||
"spam_security": [
|
||||
"예시값"
|
||||
],
|
||||
"seo": [
|
||||
"예시값"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| basic_defaults | object | `{"type":"basic","per_page":20,"per_page_mobile":15,"order…` | 저장 후 갱신된 기본 설정 카테고리. 게시판 타입, 페이지당 글 수(PC/모바일), 정렬 기준/방향, 댓글·답글 사용 여부와 깊이, 제목·내용·댓글 길이 제한, 파일 업로드 허용/용량/개수/확장자, 새 글 표시 시간, 게시판 기본 권한(default_board_permissions) 등을 포함합니다. |
|
||||
| report_policy | object | `{"auto_hide_threshold":5,"auto_hide_target":"both","daily…` | 저장 후 갱신된 신고 정책 카테고리. 자동 숨김 임계치(auto_hide_threshold)와 대상(auto_hide_target: post/comment/both), 사용자별 일일 신고 한도, 신고 거부 누적 제한(횟수/기간), 관리자·작성자 신고 알림 발송 여부와 채널을 포함합니다. |
|
||||
| spam_security | object | `{"post_cooldown_seconds":0,"comment_cooldown_seconds":0,"…` | 저장 후 갱신된 스팸·보안 카테고리. 글·댓글·신고 작성 사이의 도배 방지 쿨다운 시간(초)과 조회수 캐시 TTL을 포함합니다. |
|
||||
| display | object | `{"date_display_format":"standard"}` | 저장 후 갱신된 표시 설정 카테고리. 날짜 표시 형식(date_display_format: standard 절대 표기 / relative 상대 표기)을 포함합니다. |
|
||||
| seo | object | `{"meta_boards_title":"{site_name}","meta_boards_descripti…` | 저장 후 갱신된 SEO 메타 태그 설정 카테고리. 게시판 목록/개별 게시판/글 상세 페이지의 메타 제목·설명 템플릿과 각 페이지의 SEO 생성 활성화 여부(seo_boards, seo_board, seo_post_detail)를 포함합니다. |
|
||||
| notifications | object | `{"channels":[{"id":"mail","is_active":true,"sort_order":0…` | 저장 후 갱신된 알림 채널 설정 카테고리. 각 채널(mail, database 등)의 식별자, 활성화 여부(is_active), 정렬 순서(sort_order)를 담은 channels 배열을 포함합니다. |
|
||||
| report_permissions | object | `{"view_roles":[],"manage_roles":[]}` | 저장 후 동기화된 신고 관리 권한 역할. 신고 내역을 조회할 수 있는 역할(view_roles)과 신고를 처리·관리할 수 있는 역할(manage_roles)의 식별자 배열이며, 요청에 포함된 경우 DB 권한 역할로 동기화되어 반환됩니다. |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "sirsoft-board::messages.settings.save_success",
|
||||
"data": {
|
||||
"basic_defaults": {
|
||||
"type": "basic",
|
||||
"per_page": 20,
|
||||
"per_page_mobile": 15,
|
||||
"order_by": "created_at",
|
||||
"order_direction": "DESC",
|
||||
"secret_mode": "{MASKED}",
|
||||
"use_comment": true,
|
||||
"use_reply": true,
|
||||
"max_reply_depth": 5,
|
||||
"max_comment_depth": 10,
|
||||
"comment_order": "ASC",
|
||||
"show_view_count": true,
|
||||
"use_report": false,
|
||||
"min_title_length": 2,
|
||||
"max_title_length": 200,
|
||||
"min_content_length": 2,
|
||||
"max_content_length": 10000,
|
||||
"min_comment_length": 2,
|
||||
"max_comment_length": 1000,
|
||||
"use_file_upload": false,
|
||||
"max_file_size": 10,
|
||||
"max_file_count": 5,
|
||||
"allowed_extensions": [
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"webp",
|
||||
"pdf",
|
||||
"doc",
|
||||
"docx",
|
||||
"xls",
|
||||
"xlsx",
|
||||
"ppt",
|
||||
"pptx",
|
||||
"hwp",
|
||||
"txt",
|
||||
"zip"
|
||||
],
|
||||
"notify_admin_on_post": true,
|
||||
"notify_author": true,
|
||||
"new_display_hours": 24,
|
||||
"blocked_keywords": [
|
||||
"시발",
|
||||
"씨발",
|
||||
"ㅅㅂ",
|
||||
"ㅆㅂ",
|
||||
"좆",
|
||||
"ㅈㄴ",
|
||||
"존나",
|
||||
"씹",
|
||||
"지랄",
|
||||
"ㅈㄹ",
|
||||
"개새끼",
|
||||
"새끼",
|
||||
"ㅅㄲ",
|
||||
"개자식",
|
||||
"병신",
|
||||
"ㅂㅅ",
|
||||
"미친놈",
|
||||
"미친년",
|
||||
"느금마",
|
||||
"니애미",
|
||||
"느금",
|
||||
"엠창",
|
||||
"엠뒤",
|
||||
"빡대가리",
|
||||
"개같은",
|
||||
"개년",
|
||||
"개놈",
|
||||
"개돼지",
|
||||
"개씹",
|
||||
"개쓰레기",
|
||||
"닥쳐",
|
||||
"꺼져",
|
||||
"죽어",
|
||||
"자살해",
|
||||
"뒤져",
|
||||
"저능아",
|
||||
"정신병자",
|
||||
"썅",
|
||||
"쌍놈",
|
||||
"쌍년",
|
||||
"쉬발",
|
||||
"쒸발",
|
||||
"시팔",
|
||||
"씨팔",
|
||||
"한남",
|
||||
"한녀",
|
||||
"김치녀",
|
||||
"된장녀",
|
||||
"맘충",
|
||||
"틀딱",
|
||||
"느개비",
|
||||
"장애인놈",
|
||||
"병신새끼",
|
||||
"흑형",
|
||||
"짱깨",
|
||||
"쪽바리",
|
||||
"왜놈",
|
||||
"튀기",
|
||||
"똥남아",
|
||||
"야동",
|
||||
"포르노",
|
||||
"섹스",
|
||||
"야사",
|
||||
"음란",
|
||||
"성인사이트",
|
||||
"조건만남",
|
||||
"원나잇",
|
||||
"섹파",
|
||||
"오피",
|
||||
"키스방",
|
||||
"매춘",
|
||||
"성매매",
|
||||
"몸파는"
|
||||
],
|
||||
"default_board_permissions": {
|
||||
"admin.posts.read": [
|
||||
"admin"
|
||||
],
|
||||
"admin.posts.write": [
|
||||
"admin"
|
||||
],
|
||||
"admin.posts.read-secret": [
|
||||
"admin"
|
||||
],
|
||||
"admin.comments.read": [
|
||||
"admin"
|
||||
],
|
||||
"admin.comments.write": [
|
||||
"admin"
|
||||
],
|
||||
"admin.attachments.upload": [
|
||||
"admin"
|
||||
],
|
||||
"admin.attachments.download": [
|
||||
"admin"
|
||||
],
|
||||
"admin.manage": [
|
||||
"admin"
|
||||
],
|
||||
"posts.read": [
|
||||
"admin",
|
||||
"user",
|
||||
"guest"
|
||||
],
|
||||
"posts.write": [
|
||||
"admin",
|
||||
"user"
|
||||
],
|
||||
"posts.read-secret": [
|
||||
"admin"
|
||||
],
|
||||
"comments.read": [
|
||||
"admin",
|
||||
"user",
|
||||
"guest"
|
||||
],
|
||||
"comments.write": [
|
||||
"admin",
|
||||
"user"
|
||||
],
|
||||
"attachments.upload": [
|
||||
"admin",
|
||||
"user"
|
||||
],
|
||||
"attachments.download": [
|
||||
"admin",
|
||||
"user",
|
||||
"guest"
|
||||
],
|
||||
"manager": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"report_policy": {
|
||||
"auto_hide_threshold": 5,
|
||||
"auto_hide_target": "both",
|
||||
"daily_report_limit": 10,
|
||||
"rejection_limit_count": 5,
|
||||
"rejection_limit_days": 30,
|
||||
"notify_admin_on_report": true,
|
||||
"notify_admin_on_report_scope": "per_case",
|
||||
"notify_admin_on_report_channels": [
|
||||
"mail",
|
||||
"database"
|
||||
],
|
||||
"notify_author_on_report_action": true,
|
||||
"notify_author_on_report_action_channels": [
|
||||
"mail",
|
||||
"database"
|
||||
]
|
||||
},
|
||||
"spam_security": {
|
||||
"post_cooldown_seconds": 0,
|
||||
"comment_cooldown_seconds": 0,
|
||||
"report_cooldown_seconds": 60,
|
||||
"view_count_cache_ttl": 86400
|
||||
},
|
||||
"display": {
|
||||
"date_display_format": "standard"
|
||||
},
|
||||
"seo": {
|
||||
"meta_boards_title": "{site_name}",
|
||||
"meta_boards_description": "",
|
||||
"meta_board_title": "{board_name}",
|
||||
"meta_board_description": "{board_description}",
|
||||
"meta_post_title": "{board_name} - {post_title}",
|
||||
"meta_post_description": "",
|
||||
"seo_boards": true,
|
||||
"seo_board": true,
|
||||
"seo_post_detail": true
|
||||
},
|
||||
"notifications": {
|
||||
"channels": [
|
||||
{
|
||||
"id": "mail",
|
||||
"is_active": true,
|
||||
"sort_order": 0
|
||||
},
|
||||
{
|
||||
"id": "database",
|
||||
"is_active": true,
|
||||
"sort_order": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"report_permissions": {
|
||||
"view_roles": [],
|
||||
"manage_roles": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.settings.update`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 게시판 모듈의 환경설정을 저장합니다. `auth:sanctum` 인증과 `sirsoft-board.settings.update` 권한이 필요합니다. `_tab`으로 지정한 탭 단위로 검증된 설정을 저장하며, `report_permissions`가 포함된 경우 신고 권한 역할도 함께 동기화합니다. 저장 성공 시 갱신된 전체 설정과 신고 권한 역할을 반환합니다.
|
||||
|
||||
|
||||
### POST /api/modules/sirsoft-board/admin/settings/bulk-apply
|
||||
<!-- @generated:start: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 | 아니오 | — | 환경설정 기본값 대신 사용할 재정의 값 맵. 지정한 필드에 대해 기본값이 아닌 임의의 값으로 일괄 적용할 때 사용합니다. |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/modules/sirsoft-board/admin/settings/bulk-apply HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"fields": [
|
||||
"예시값"
|
||||
],
|
||||
"apply_all": true,
|
||||
"board_ids": [
|
||||
"예시값"
|
||||
],
|
||||
"override_values": [
|
||||
"예시값"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.settings.update`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 환경설정의 기본값을 기존 게시판들에 일괄 적용합니다. `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
|
||||
<!-- @generated:start: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`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/modules/sirsoft-board/admin/settings/clear-cache HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.settings.update`)이 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 게시판 모듈 설정 캐시를 초기화합니다. `auth:sanctum` 인증과 `sirsoft-board.settings.update` 권한이 필요합니다. ModuleSettings 캐시와 게시판 캐시를 모두 초기화하며, 응답으로 `cleared: true`를 반환합니다.
|
||||
|
||||
|
||||
### GET /api/modules/sirsoft-board/admin/settings/{category}
|
||||
<!-- @generated:start:api.modules.sirsoft-board.admin.settings.show -->
|
||||
- **라우트명**: `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 | 예 | — | 분류 필터 (해당 분류의 항목만 조회) |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/admin/settings/{category} HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.settings.read`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 특정 카테고리의 설정만 조회합니다. `auth:sanctum` 인증과 `sirsoft-board.settings.read` 권한이 필요합니다. 경로의 `category`에 해당하는 설정만 반환하며, 응답에는 카테고리명(category), 설정값(settings), 현재 사용자의 수정 가능 여부(abilities)가 포함됩니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.modules.sirsoft-board.users.posts.index -->
|
||||
- **라우트명**: `api.modules.sirsoft-board.users.posts.index`
|
||||
- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@userPosts`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| user | path | string | 예 | — | 대상 user의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/users/a234c2b1-cde8-437f-b28b-23323be2b98d/posts HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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 모드 글은 태그를 제거한 평문으로 변환되며, 목록 미리보기용으로 사용합니다. |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "게시글 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"current_page": 1,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"board_slug": "apidoc-sample-board",
|
||||
"board_name": "API 문서 샘플 게시판",
|
||||
"activity_type": "authored",
|
||||
"activity_count": 0,
|
||||
"title": "API 문서 샘플 게시글",
|
||||
"is_secret": "{MASKED}",
|
||||
"status": "published",
|
||||
"view_count": 44,
|
||||
"comment_count": 0,
|
||||
"created_at": "2026-07-08 10:41:34",
|
||||
"created_at_formatted": "4시간 전",
|
||||
"content_plain": "API 레퍼런스 실측용 완전 샘플 게시글 본문입니다."
|
||||
}
|
||||
],
|
||||
"first_page_url": "https://api.example.com/api/modules/sirsoft-board/users/a234c2b1-cde8-437f-b28b-23323be2b98d/posts?page=1",
|
||||
"from": 1,
|
||||
"last_page": 1,
|
||||
"last_page_url": "https://api.example.com/api/modules/sirsoft-board/users/a234c2b1-cde8-437f-b28b-23323be2b98d/posts?page=1",
|
||||
"links": [
|
||||
{
|
||||
"url": null,
|
||||
"label": "pagination.previous",
|
||||
"page": null,
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"url": "https://api.example.com/api/modules/sirsoft-board/users/a234c2b1-cde8-437f-b28b-23323be2b98d/posts?page=1",
|
||||
"label": "1",
|
||||
"page": 1,
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"url": null,
|
||||
"label": "pagination.next",
|
||||
"page": null,
|
||||
"active": false
|
||||
}
|
||||
],
|
||||
"next_page_url": null,
|
||||
"path": "https://api.example.com/api/modules/sirsoft-board/users/a234c2b1-cde8-437f-b28b-23323be2b98d/posts",
|
||||
"per_page": 25,
|
||||
"prev_page_url": null,
|
||||
"to": 1,
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 특정 사용자(`{user}` 는 회원 uuid 로 라우트 바인딩)의 공개 프로필 페이지에 표시할 게시글 목록을 모든 게시판에 걸쳐 반환합니다. 비밀글은 제외되며, `optional.sanctum` 이 적용되어 비로그인 상태에서도 조회할 수 있습니다. `per_page`(1~100, 기본 20)와 `sort`(latest 등) 쿼리 파라미터로 페이지네이션·정렬을 제어합니다.
|
||||
|
||||
|
||||
### GET /api/modules/sirsoft-board/users/{user}/posts/stats
|
||||
<!-- @generated:start:api.modules.sirsoft-board.users.posts.stats -->
|
||||
- **라우트명**: `api.modules.sirsoft-board.users.posts.stats`
|
||||
- **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\User\PostController@userPostsStats`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| user | path | string | 예 | — | 대상 user의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-board/users/a234c2b1-cde8-437f-b28b-23323be2b98d/posts/stats HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| posts_count | integer | `1` | posts 개수 (집계) |
|
||||
| comments_count | integer | `1` | comments 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "성공적으로 처리되었습니다.",
|
||||
"data": {
|
||||
"posts_count": 1,
|
||||
"comments_count": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 공개 프로필 페이지 상단에 표시할 특정 사용자(`{user}` 는 회원 uuid)의 게시글·댓글 수 요약을 반환합니다. `status=published` 인 항목만 집계하며, `optional.sanctum` 이 적용되어 비로그인 상태에서도 조회할 수 있습니다.
|
||||
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Support\ApiDoc;
|
||||
|
||||
use App\Contracts\ApiDoc\ApiDocSampleSeeder;
|
||||
use App\Models\User;
|
||||
use Modules\Sirsoft\Board\Enums\PostStatus;
|
||||
use Modules\Sirsoft\Board\Enums\ReportReasonType;
|
||||
use Modules\Sirsoft\Board\Enums\ReportStatus;
|
||||
use Modules\Sirsoft\Board\Enums\ReportType;
|
||||
use Modules\Sirsoft\Board\Enums\TriggerType;
|
||||
use Modules\Sirsoft\Board\Models\Attachment;
|
||||
use Modules\Sirsoft\Board\Models\Board;
|
||||
use Modules\Sirsoft\Board\Models\BoardType;
|
||||
use Modules\Sirsoft\Board\Models\Comment;
|
||||
use Modules\Sirsoft\Board\Models\Post;
|
||||
use Modules\Sirsoft\Board\Models\Report;
|
||||
use Modules\Sirsoft\Board\Services\BoardPermissionService;
|
||||
|
||||
/**
|
||||
* sirsoft-board API 문서 실측용 완전 샘플 시더
|
||||
*
|
||||
* 게시판 도메인은 라우트가 route-model binding 대신 게시판 slug·게시글 id 를
|
||||
* 문자열 path 파라미터로 받으므로(boards/{slug}/posts/{id}), docgen 의 route key
|
||||
* 자동 치환이 상세 GET 에서 동작하지 않습니다. 이 시더는 완전 샘플 게시판
|
||||
* (공개 게시글 + 댓글 + 첨부)을 멱등 생성하고, 각 도메인의 `path_params` 맵으로
|
||||
* slug/id/hash 를 실제 값으로 치환할 수 있게 하여 상세 조회를 실측 가능하게 합니다.
|
||||
*
|
||||
* `api:docgen --scope=module:sirsoft-board` 실행 시 커맨드가 규약 위치
|
||||
* (`Modules\Sirsoft\Board\Support\ApiDoc\ApiDocSampleService`)로 자동 발견합니다.
|
||||
*/
|
||||
class ApiDocSampleService implements ApiDocSampleSeeder
|
||||
{
|
||||
/**
|
||||
* @var string 샘플 게시판 식별용 슬러그 마커
|
||||
*/
|
||||
private const SAMPLE_SLUG = 'apidoc-sample-board';
|
||||
|
||||
/**
|
||||
* 게시판 도메인 완전 샘플을 멱등 생성하고 도메인별 대표 route key + path_params 맵을 반환합니다.
|
||||
*
|
||||
* @return array<string, array{model: class-string, key: string, value: string, path_params?: array<string, string>}> 도메인 => 대표 레코드 정보
|
||||
*/
|
||||
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' => '<p>API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.</p>',
|
||||
'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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Tests\Unit\Support;
|
||||
|
||||
// ModuleTestCase를 수동으로 require (autoload 전에 로드 필요)
|
||||
require_once __DIR__.'/../../ModuleTestCase.php';
|
||||
|
||||
use App\Contracts\ApiDoc\ApiDocSampleSeeder;
|
||||
use Modules\Sirsoft\Board\Models\Attachment;
|
||||
use Modules\Sirsoft\Board\Models\Board;
|
||||
use Modules\Sirsoft\Board\Models\Comment;
|
||||
use Modules\Sirsoft\Board\Models\Post;
|
||||
use Modules\Sirsoft\Board\Support\ApiDoc\ApiDocSampleService;
|
||||
use Modules\Sirsoft\Board\Tests\ModuleTestCase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
|
||||
/**
|
||||
* sirsoft-board API 문서 실측용 완전 샘플 시더 테스트.
|
||||
*
|
||||
* 게시판 도메인 대표 샘플(공개 게시글 + 댓글 + 첨부)이 생성되고, slug 라우팅
|
||||
* 실측을 위한 도메인별 path_params 맵이 반환되며, 재실행 시 멱등한지 검증한다.
|
||||
*/
|
||||
class ApiDocSampleServiceTest extends ModuleTestCase
|
||||
{
|
||||
/**
|
||||
* @var string 샘플 게시판 슬러그 마커
|
||||
*/
|
||||
private const SAMPLE_SLUG = 'apidoc-sample-board';
|
||||
|
||||
#[Test]
|
||||
public function 시더는_계약을_구현한다(): void
|
||||
{
|
||||
$this->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());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,24 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.2] - 2026-07-10
|
||||
|
||||
### Changed
|
||||
|
||||
- 쇼핑몰 환경설정 > 배송설정에서 배송가능 국가를 추가할 때, 사이트에 설치된 모든 언어에 대해 국가명을 입력할 수 있습니다. 기존에는 한국어·영어 두 칸만 있어 그 외 언어의 국가명을 넣을 방법이 없었습니다. 언어를 새로 설치하면 입력칸도 함께 늘어납니다. 입력하지 않은 언어는 저장되지 않으며, 기본 제공 국가는 언어팩이 자동으로 채웁니다.
|
||||
- 휴대폰·태블릿 화면에서 통화·배송국가 선택이 메뉴 안의 접었다 펼치는 항목으로 바뀌었습니다. 접혀 있을 때도 현재 선택된 통화와 배송국가가 함께 표시되어 펼치지 않아도 확인할 수 있고, 펼치면 가로 버튼으로 나열됩니다. 좁은 화면의 메뉴 안에서 드롭다운이 잘려 보이던 문제가 함께 해소됩니다. 데스크톱 화면의 드롭다운은 그대로입니다. 관리자 화면도 동일하게 적용됩니다.
|
||||
- 휴대폰 화면에서 주문 취소·주문상태 일괄변경 팝업의 상품 목록이 표 대신 상품별 카드 형태로 표시됩니다.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 배송국가 이름이 어떤 언어로 화면을 보든 항상 한국어로만 표시되던 문제를 수정했습니다. 이제 사용자·관리자 헤더의 배송국가 선택, 회원가입의 배송국가 선택, 관리자 회원정보와 주문상세의 배송국가, 배송설정의 기본 배송국가가 모두 현재 보고 있는 언어로 표시됩니다. 일본어처럼 나중에 추가한 언어에서도 언어팩이 제공하는 국가명이 그대로 반영됩니다.
|
||||
- 쇼핑몰 환경설정의 은행 관리에서 은행코드를 입력해도 입력한 값이 보이지 않던 문제를 수정했습니다 — 좁은 화면에서 입력칸이 글자가 들어갈 수 없을 만큼 줄어들고 있었습니다.
|
||||
- 휴대폰 화면에서 주문 취소·주문상태 일괄변경 팝업의 금액이 "249,000원" 대신 "249,000" / "원" 처럼 숫자 중간에서 잘려 보이던 문제를 수정했습니다. 금액은 앞으로도 숫자 중간에서 줄바꿈되지 않으며, "결제완료" 같은 상태 이름도 "결제완" / "료" 로 쪼개지지 않습니다.
|
||||
- 휴대폰 화면에서 쇼핑몰 환경설정 화면이 좌우로 넘쳐 가로 스크롤이 생기던 문제를 수정했습니다 (이메일 입력 줄, 쇼핑몰 이름·종목 입력칸, 탭 영역).
|
||||
- 휴대폰 화면에서 쿠폰 발급기간 입력칸과 엑셀 다운로드 기간 검색 입력칸이 화면 밖으로 넘치던 문제를 수정했습니다.
|
||||
- 은행 관리·도서산간 배송비 팝업의 표가 좁은 화면에서 가로로 스크롤되도록 명시해, 앞으로 표가 잘려 보이지 않도록 했습니다.
|
||||
- 관리자 주문상세의 상품 목록에서 구매수량·적립예정만 가운데로 놓여 다른 금액 열과 세로줄이 어긋나 보이던 문제를 수정했습니다. 이제 여섯 개 숫자 열이 모두 오른쪽 끝에 맞춰 정렬됩니다. 휴대폰 화면의 카드 형태에서도 동일하게 적용됩니다.
|
||||
|
||||
## [1.0.1] - 2026-07-08
|
||||
|
||||
### Added
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "modules/sirsoft-ecommerce",
|
||||
"description": "Ecommerce module for Gnuboard7",
|
||||
"type": "library",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# API 레퍼런스 문서 목차
|
||||
|
||||
> **소유**: 모듈 `sirsoft-ecommerce` · **생성**: `php artisan api:docgen` (실측 기반).
|
||||
> 아래 표는 자동 생성됩니다. 각 문서를 열면 엔드포인트별 파라미터·응답·예시를 볼 수 있습니다.
|
||||
|
||||
<!-- @generated:start:api-readme-index -->
|
||||
- **문서 수**: 33 · **엔드포인트 수**: 231
|
||||
|
||||
| 문서 | 도메인 | 엔드포인트 |
|
||||
| --- | --- | --- |
|
||||
| [addresses.md](addresses.md) | `addresses` | 6 |
|
||||
| [brands.md](brands.md) | `brands` | 6 |
|
||||
| [cart.md](cart.md) | `cart` | 11 |
|
||||
| [categories.md](categories.md) | `categories` | 14 |
|
||||
| [category-image.md](category-image.md) | `category-image` | 1 |
|
||||
| [checkout.md](checkout.md) | `checkout` | 5 |
|
||||
| [claim-reasons.md](claim-reasons.md) | `claim-reasons` | 8 |
|
||||
| [coupons.md](coupons.md) | `coupons` | 4 |
|
||||
| [currency.md](currency.md) | `currency` | 2 |
|
||||
| [dashboard.md](dashboard.md) | `dashboard` | 4 |
|
||||
| [extra-fee-templates.md](extra-fee-templates.md) | `extra-fee-templates` | 10 |
|
||||
| [guest.md](guest.md) | `guest` | 5 |
|
||||
| [inquiries.md](inquiries.md) | `inquiries` | 10 |
|
||||
| [mileage.md](mileage.md) | `mileage` | 3 |
|
||||
| [mileage-transactions.md](mileage-transactions.md) | `mileage-transactions` | 5 |
|
||||
| [options.md](options.md) | `options` | 3 |
|
||||
| [orders.md](orders.md) | `orders` | 22 |
|
||||
| [payments.md](payments.md) | `payments` | 1 |
|
||||
| [presets.md](presets.md) | `presets` | 4 |
|
||||
| [product-common-infos.md](product-common-infos.md) | `product-common-infos` | 6 |
|
||||
| [product-image.md](product-image.md) | `product-image` | 1 |
|
||||
| [product-labels.md](product-labels.md) | `product-labels` | 6 |
|
||||
| [product-notice-templates.md](product-notice-templates.md) | `product-notice-templates` | 7 |
|
||||
| [products.md](products.md) | `products` | 30 |
|
||||
| [promotion-coupons.md](promotion-coupons.md) | `promotion-coupons` | 9 |
|
||||
| [review-image.md](review-image.md) | `review-image` | 1 |
|
||||
| [reviews.md](reviews.md) | `reviews` | 12 |
|
||||
| [settings.md](settings.md) | `settings` | 10 |
|
||||
| [shipping-carriers.md](shipping-carriers.md) | `shipping-carriers` | 7 |
|
||||
| [shipping-country.md](shipping-country.md) | `shipping-country` | 2 |
|
||||
| [shipping-policies.md](shipping-policies.md) | `shipping-policies` | 11 |
|
||||
| [users.md](users.md) | `users` | 2 |
|
||||
| [wishlist.md](wishlist.md) | `wishlist` | 3 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
@@ -0,0 +1,555 @@
|
||||
# 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
|
||||
<!-- @generated:start:api.modules.sirsoft-ecommerce.user.addresses.index -->
|
||||
- **라우트명**: `api.modules.sirsoft-ecommerce.user.addresses.index`
|
||||
- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserAddressController@index`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-ecommerce/user/addresses HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| addresses | object | `{"data":[{"id":263,"user_id":"a1e0a91a-fba6-491c-a53e-728…` | 회원 본인 소유 배송지 컬렉션 (`data[]` 배송지 항목 배열 + `abilities.can_create` — UserAddressCollection 파생) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "배송지 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"addresses": {
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 배송지",
|
||||
"recipient_name": "원진수",
|
||||
"recipient_phone": "010-4988-7051",
|
||||
"country_code": "KR",
|
||||
"country_name": {
|
||||
"ko": "한국",
|
||||
"en": "South Korea"
|
||||
},
|
||||
"zipcode": "27058",
|
||||
"address": "충청남도 남양주시 테헤란로 6114",
|
||||
"address_detail": "Quidem natus iste.",
|
||||
"address_line_1": null,
|
||||
"address_line_2": null,
|
||||
"city": null,
|
||||
"state": null,
|
||||
"postal_code": null,
|
||||
"is_default": false,
|
||||
"is_domestic": true,
|
||||
"is_international": false,
|
||||
"full_address": "충청남도 남양주시 테헤란로 6114 Quidem natus iste.",
|
||||
"created_at": "2026-07-08 10:44:49",
|
||||
"updated_at": "2026-07-08 10:44:49",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true,
|
||||
"can_set_default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"abilities": {
|
||||
"can_create": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 로그인한 회원 본인의 배송지 목록을 조회합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::getUserAddresses()`가 현재 사용자(`Auth::id()`) 소유의 배송지를 조회해 `UserAddressCollection`으로 반환합니다. 마이페이지 배송지 관리 화면이나 주문 시 배송지 선택 목록을 채우는 용도이며, 다른 회원의 배송지는 노출되지 않습니다.
|
||||
|
||||
|
||||
### POST /api/modules/sirsoft-ecommerce/user/addresses
|
||||
<!-- @generated:start:api.modules.sirsoft-ecommerce.user.addresses.store -->
|
||||
- **라우트명**: `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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/modules/sirsoft-ecommerce/user/addresses HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "예시 이름",
|
||||
"recipient_name": "예시 이름",
|
||||
"recipient_phone": "010-1234-5678",
|
||||
"country_code": "KR",
|
||||
"zipcode": "06234",
|
||||
"province_code": "예시값",
|
||||
"city": "예시값",
|
||||
"address": "서울특별시 강남구 테헤란로 1",
|
||||
"address_detail": "서울특별시 강남구 테헤란로 1",
|
||||
"address_type_code": "R",
|
||||
"address_line_1": "서울특별시 강남구 테헤란로 1",
|
||||
"address_line_2": "서울특별시 강남구 테헤란로 1",
|
||||
"intl_city": "예시값",
|
||||
"intl_state": "예시값",
|
||||
"intl_postal_code": "06234",
|
||||
"is_default": true,
|
||||
"force_overwrite": true
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| address | object | `{"id":9,"user_id":"a234c2b1-cde8-437f-b28b-23323be2b98d",…` | 기본주소 (국내) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 201
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "배송지가 추가되었습니다.",
|
||||
"data": {
|
||||
"address": {
|
||||
"id": 9,
|
||||
"user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "실측 예시값",
|
||||
"recipient_name": "실측 예시값",
|
||||
"recipient_phone": "실측 예시값",
|
||||
"country_code": "KR",
|
||||
"country_name": {
|
||||
"ko": "한국",
|
||||
"en": "South Korea"
|
||||
},
|
||||
"zipcode": "실측 예시값",
|
||||
"address": "실측 예시값",
|
||||
"address_detail": "실측 예시값",
|
||||
"address_line_1": null,
|
||||
"address_line_2": null,
|
||||
"city": "실측 예시값",
|
||||
"state": null,
|
||||
"postal_code": null,
|
||||
"is_default": true,
|
||||
"is_domestic": true,
|
||||
"is_international": false,
|
||||
"full_address": "실측 예시값 실측 예시값",
|
||||
"created_at": "2026-07-08 15:00:36",
|
||||
"updated_at": "2026-07-08 15:00:36",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": false,
|
||||
"can_set_default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 로그인한 회원 본인의 새 배송지를 등록합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::createAddress()`가 검증된 요청에 현재 사용자 ID를 결합해 배송지를 생성하고 성공 시 `201`로 반환합니다. 국내(우편번호/도로명·지번)·해외(intl_* 필드) 주소를 모두 지원하고 `is_default`로 기본 배송지 지정이 가능합니다. 같은 이름의 배송지가 있으면 `409`(중복 ID 포함)를, `force_overwrite`로 덮어쓰기를 허용할 수 있으며, 최대 배송지 개수를 초과하면 `422`를 반환합니다.
|
||||
|
||||
|
||||
### DELETE /api/modules/sirsoft-ecommerce/user/addresses/{id}
|
||||
<!-- @generated:start:api.modules.sirsoft-ecommerce.user.addresses.destroy -->
|
||||
- **라우트명**: `api.modules.sirsoft-ecommerce.user.addresses.destroy`
|
||||
- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserAddressController@destroy`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| id | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/modules/sirsoft-ecommerce/user/addresses/1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
|
||||
|
||||
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "배송지가 삭제되었습니다.",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 로그인한 회원 본인의 배송지 1건을 삭제합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::deleteAddress()`가 현재 사용자 소유 여부를 확인한 뒤 path의 `{id}` 배송지를 삭제합니다. 본인 소유가 아니거나 존재하지 않는 배송지이면 `404`를 반환합니다. 마이페이지 배송지 관리에서 더 이상 사용하지 않는 배송지를 제거하는 용도입니다.
|
||||
|
||||
|
||||
### GET /api/modules/sirsoft-ecommerce/user/addresses/{id}
|
||||
<!-- @generated:start:api.modules.sirsoft-ecommerce.user.addresses.show -->
|
||||
- **라우트명**: `api.modules.sirsoft-ecommerce.user.addresses.show`
|
||||
- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserAddressController@show`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| id | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-ecommerce/user/addresses/1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| address | object | `{"id":1,"user_id":"a234c2b1-cde8-437f-b28b-23323be2b98d",…` | 기본주소 (국내) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "배송지 정보를 조회했습니다.",
|
||||
"data": {
|
||||
"address": {
|
||||
"id": 1,
|
||||
"user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 배송지",
|
||||
"recipient_name": "원진수",
|
||||
"recipient_phone": "010-4988-7051",
|
||||
"country_code": "KR",
|
||||
"country_name": {
|
||||
"ko": "한국",
|
||||
"en": "South Korea"
|
||||
},
|
||||
"zipcode": "27058",
|
||||
"address": "충청남도 남양주시 테헤란로 6114",
|
||||
"address_detail": "Quidem natus iste.",
|
||||
"address_line_1": null,
|
||||
"address_line_2": null,
|
||||
"city": null,
|
||||
"state": null,
|
||||
"postal_code": null,
|
||||
"is_default": false,
|
||||
"is_domestic": true,
|
||||
"is_international": false,
|
||||
"full_address": "충청남도 남양주시 테헤란로 6114 Quidem natus iste.",
|
||||
"created_at": "2026-07-08 10:44:49",
|
||||
"updated_at": "2026-07-08 10:44:49",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true,
|
||||
"can_set_default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 로그인한 회원 본인의 배송지 1건 상세를 조회합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::getAddress()`가 현재 사용자 소유의 path `{id}` 배송지를 조회해 `UserAddressResource`로 반환합니다. 본인 소유가 아니거나 존재하지 않는 배송지이면 `404`를 반환합니다. 배송지 수정 화면 진입 시 기존 값을 불러오는 용도입니다.
|
||||
|
||||
|
||||
### PUT /api/modules/sirsoft-ecommerce/user/addresses/{id}
|
||||
<!-- @generated:start:api.modules.sirsoft-ecommerce.user.addresses.update -->
|
||||
- **라우트명**: `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`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PUT /api/modules/sirsoft-ecommerce/user/addresses/1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "예시 이름",
|
||||
"recipient_name": "예시 이름",
|
||||
"recipient_phone": "010-1234-5678",
|
||||
"country_code": "KR",
|
||||
"zipcode": "06234",
|
||||
"province_code": "예시값",
|
||||
"city": "예시값",
|
||||
"address": "서울특별시 강남구 테헤란로 1",
|
||||
"address_detail": "서울특별시 강남구 테헤란로 1",
|
||||
"address_type_code": "R",
|
||||
"address_line_1": "서울특별시 강남구 테헤란로 1",
|
||||
"address_line_2": "서울특별시 강남구 테헤란로 1",
|
||||
"intl_city": "예시값",
|
||||
"intl_state": "예시값",
|
||||
"intl_postal_code": "06234",
|
||||
"is_default": true
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| address | object | `{"id":1,"user_id":"a234c2b1-cde8-437f-b28b-23323be2b98d",…` | 기본주소 (국내) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "배송지가 수정되었습니다.",
|
||||
"data": {
|
||||
"address": {
|
||||
"id": 1,
|
||||
"user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "실측 예시값",
|
||||
"recipient_name": "실측 예시값",
|
||||
"recipient_phone": "실측 예시값",
|
||||
"country_code": "KR",
|
||||
"country_name": {
|
||||
"ko": "한국",
|
||||
"en": "South Korea"
|
||||
},
|
||||
"zipcode": "실측 예시값",
|
||||
"address": "실측 예시값",
|
||||
"address_detail": "실측 예시값",
|
||||
"address_line_1": null,
|
||||
"address_line_2": null,
|
||||
"city": "실측 예시값",
|
||||
"state": null,
|
||||
"postal_code": null,
|
||||
"is_default": true,
|
||||
"is_domestic": true,
|
||||
"is_international": false,
|
||||
"full_address": "실측 예시값 실측 예시값",
|
||||
"created_at": "2026-07-08 10:44:49",
|
||||
"updated_at": "2026-07-08 15:00:36",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": false,
|
||||
"can_set_default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 로그인한 회원 본인의 배송지 1건을 수정합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::updateAddress()`가 현재 사용자 소유의 path `{id}` 배송지를 검증된 값으로 갱신하고 `UserAddressResource`로 반환합니다. 모든 본문 필드는 선택이며 전달된 필드만 갱신되고, 국내·해외 주소 필드와 `is_default`(기본 배송지 지정)를 모두 지원합니다. 본인 소유가 아니거나 존재하지 않는 배송지이면 `404`를 반환합니다.
|
||||
|
||||
|
||||
### PATCH /api/modules/sirsoft-ecommerce/user/addresses/{id}/default
|
||||
<!-- @generated:start:api.modules.sirsoft-ecommerce.user.addresses.set-default -->
|
||||
- **라우트명**: `api.modules.sirsoft-ecommerce.user.addresses.set-default`
|
||||
- **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\User\UserAddressController@setDefault`
|
||||
- **인증/권한**: `auth:sanctum`
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| id | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PATCH /api/modules/sirsoft-ecommerce/user/addresses/1/default HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
|
||||
|
||||
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "기본 배송지로 설정되었습니다.",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 로그인한 회원 본인의 배송지 1건을 기본 배송지로 지정합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::setDefaultAddress()`가 현재 사용자 소유의 path `{id}` 배송지를 기본으로 설정하고 기존 기본 배송지는 자동 해제됩니다. 본인 소유가 아니거나 존재하지 않는 배송지이면 `404`를 반환합니다. 마이페이지 배송지 목록에서 기본 배송지를 전환하는 용도입니다.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user