fix(core,ecommerce,board,page,admin_basic,basic): 목록 응답의 하위 컬렉션 전량 직렬화 해소
관리자 상품목록을 한 페이지 여는 것만으로 그 페이지 모든 상품의 옵션이 응답에 실렸다. 같은 패턴을 저장소 전역에서 찾아 14개 목록 엔드포인트를 함께 정리했다. 근본 원인은 둘이다. Resource 가 whenLoaded 로 방어하는데 Repository 가 목록 쿼리에서 관계를 무조건 로드해 가드가 항상 참이 되는 가짜 가드, 그리고 toListArray 경량 표현을 정의해 두고도 컬렉션이 toArray 를 부르는 목록/상세 미분리다. 둘 다 응답만 보면 정상이라 오류도 경고도 없이 페이로드만 불어난다. 목록은 화면이 실제로 그리는 것만 싣는다. 개수·합계는 PHP 컬렉션 연산이 아니라 DB 집계로, 대표 1건이 필요한 곳은 관계 자체를 oldestOfMany 로 좁힌다. eager load 의 limit(1) 은 부모별이 아니라 배치 쿼리 전체에 걸려 첫 행만 값을 갖게 되므로 쓸 수 없다. 뺀 값에는 대체 경로를 먼저 만들었다. 상품 옵션은 행을 펼칠 때 배치로 불러오고(상품 수와 무관하게 쿼리 상수), 종전 동작이 필요한 호출자를 위해 ?with_options=1 등 opt-in 을 남겼다. 배송정책 국가설정과 리뷰 첨부 이미지는 소비처를 실측한 결과 화면이 실제로 그리고 있어 제거하지 않았다 — 그 소비 사실을 회귀 테스트로 고정했다. 재발 방지로 정적 검사 룰 4종과 규정 문서 항목을 함께 넣었다.
This commit is contained in:
@@ -356,6 +356,24 @@ Icon 은 `<i>` 글리프라 박스 크기가 곧 `font-size` 다. `w-N h-N` 은
|
||||
|
||||
> 상세: [validation.md "계층 리소스 순환 참조" / "배열 항목의 상위 스코프"](docs/backend/validation.md), [service-repository.md "중첩 리소스 스코프" / "설정 기반 한계값"](docs/backend/service-repository.md)
|
||||
|
||||
### 목록 응답의 하위 컬렉션
|
||||
|
||||
목록은 화면이 그 행에서 **실제로 그리는 것**만 싣는다. 행마다 하위 컬렉션을 통째로 직렬화하면 한 페이지를 여는 것만으로 수백~수천 행이 응답에 실린다 (공개 #76 — 상품 100건 × 옵션 20건).
|
||||
|
||||
| 금지 | 올바른 사용 |
|
||||
|------|------------|
|
||||
| `relationLoaded('x') ? $this->x : $this->whenLoaded('y')` (가짜 가드) | `whenLoaded('x', fn () => ...)` 하나만 — 로드 여부는 **Repository 가** 결정한다 |
|
||||
| Resource 는 `whenLoaded` 로 방어하는데 Repository 가 목록에서 무조건 eager load | 목록의 `relations:` 는 목록이 **실제로 직렬화하는** 관계만 |
|
||||
| 개수/합계를 PHP 컬렉션 연산으로 (`$this->options->where(...)->sum(...)`) | `withCount:` / `withSum`(`outerUsing:`) DB 집계 |
|
||||
| `toListArray()` 를 정의해 두고 컬렉션이 `toArray()` 를 호출 | 컨트롤러/컬렉션이 목록 표현을 **명시 호출** |
|
||||
| 목록 Resource 안에서 관계 재쿼리 (`$this->images()->first()`) | `relationLoaded` 분기로 로드된 컬렉션에서 고른다 |
|
||||
| 집계 별칭 존재 여부를 `!== null` 로 판정 | `array_key_exists($alias, $model->getAttributes())` — SUM 은 0건에서 NULL 이라 값 검사로는 "집계 안 함" 과 구분되지 않는다 |
|
||||
| 목록에서 뺀 값을 대체 경로 없이 제거 | 지연 로드 경로(배치 조회)를 먼저 만들고, 하위호환은 opt-in 파라미터(`?with_options=1`)로 |
|
||||
|
||||
착수 전 **소비처를 실측**한다. 화면이 그 값을 실제로 순회·렌더하면 제거는 기능 축소다 — 계획서에 "안 쓴다" 고 적혀 있어도 레이아웃 JSON 을 열어 확인한다.
|
||||
|
||||
> 상세: [api-resources.md](docs/backend/api-resources.md), [service-repository.md](docs/backend/service-repository.md)
|
||||
|
||||
### 목록 조회 컬럼 프루닝과 지연 조인
|
||||
|
||||
| 금지 | 올바른 사용 |
|
||||
|
||||
@@ -72,6 +72,13 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- 레이아웃 편집기의 파일 목록·버전 이력·확장 목록이 목록에 필요한 정보만 받아오도록 바꿨습니다. 예전에는 목록 한 번에 그 템플릿의 모든 레이아웃 본문과 버전마다의 본문 사본까지 함께 실려, 레이아웃이 많거나 저장을 자주 한 템플릿일수록 편집기 진입이 느려졌습니다. 편집 본문은 파일을 선택할 때 받아오므로 화면에 보이는 내용은 이전과 동일합니다. 버전 이력은 최근 100건까지 표시하며, 더 필요하면 조회 건수를 지정할 수 있습니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 역할 관리 목록과 메뉴 관리 목록이 화면에서 쓰지 않는 정보를 함께 내려주던 것을 정리했습니다. 역할 목록은 권한 전체 목록을 세 가지 형태로 중복해서, 메뉴 목록은 하위 메뉴마다 역할 정보를 함께 싣고 있었습니다. 역할의 권한은 역할 수정 화면에서 종전대로 확인·편집할 수 있습니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 레이아웃 버전 목록 조회에 건수 제한을 두었습니다. 허용 범위를 벗어난 값을 지정하면 안내 문구가 표시됩니다. 버전 이력의 '더 보기'는 조회 상한에 이르면 더 넓히지 않습니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 알림 정의 목록이 지금 보고 있는 채널의 템플릿만 받아오도록 바꿨습니다. 예전에는 정의마다 모든 채널의 제목과 본문이 함께 실려, 채널을 여러 개 쓰는 사이트일수록 목록이 무거웠습니다. 채널을 지정하지 않고 목록을 조회하면 템플릿 본문은 함께 오지 않으며, 전체 채널의 내용이 필요하면 정의 단건 조회를 이용하면 됩니다. 화면에 보이는 내용과 '되돌리기' 버튼 표시 조건은 이전과 동일합니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 본인인증 메시지 정의 목록이 대표 템플릿 한 건만 받아오도록 바꿨습니다. 전체 템플릿은 정의를 열었을 때 종전대로 받아옵니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 역할 목록에 할당된 권한 개수를 함께 표시할 수 있도록 집계 값을 제공합니다. 권한 목록 자체를 싣지 않고도 규모를 확인할 수 있습니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
|
||||
- 활동 로그·알림 발송 이력·본인인증 기록·스케줄 실행 이력·회원 목록을 뒤쪽 페이지에서도 빠르게 열 수 있도록 조회 방식을 바꿨습니다. 예전에는 페이지가 뒤로 갈수록 건너뛰는 기록의 본문·변경 내역까지 함께 읽어 느려졌지만, 이제 현재 페이지에 해당하는 기록만 상세 정보를 읽습니다. 화면에 보이는 내용은 이전과 동일합니다. (#74 @jiwonpapa 님께서 제보해주셨습니다.)
|
||||
- 확장이 업로드 허용 형식을 넓힐 수 있도록 `core.attachment.allowed_extensions` 필터를 추가했습니다. 관리자 설정 목록을 기준으로, 확장이 자기 기능에 필요한 형식만 덧붙일 수 있습니다.
|
||||
- 활동 로그·알림 발송 이력·회원 목록의 정렬 색인을 정비했습니다. 같은 시각에 쌓인 기록이 많은 구간에서 순서를 정하느라 생기던 추가 작업이 사라져, 기록이 많은 사이트일수록 목록이 빨라집니다. 기존 사이트도 업데이트 시 자동으로 반영되며, 기록이 아주 많은 경우 이 과정에 수 분이 걸리고 그동안 해당 기록의 저장이 잠시 대기할 수 있습니다.
|
||||
|
||||
@@ -24,7 +24,20 @@ interface LayoutExtensionVersionRepositoryInterface
|
||||
* @param int $extensionId 레이아웃 확장 ID
|
||||
* @return Collection 버전 컬렉션
|
||||
*/
|
||||
public function getVersions(int $extensionId): Collection;
|
||||
public function getVersions(int $extensionId, int $limit = 100): Collection;
|
||||
|
||||
/**
|
||||
* 확장의 특정 버전 번호 조회 (본문 포함)
|
||||
*
|
||||
* 목록(getVersions)은 경량 조회라 `content` 를 담지 않고 건수 상한이 있다. 단건은 그
|
||||
* 대체 경로이므로 반드시 전용 조회를 쓴다 — 목록 조회를 재사용하면 본문이 사라지고
|
||||
* 상한 밖의 오래된 버전을 찾지 못한다.
|
||||
*
|
||||
* @param int $extensionId 레이아웃 확장 ID
|
||||
* @param int $version 버전 번호
|
||||
* @return TemplateLayoutExtensionVersion|null 찾은 버전 모델 또는 null
|
||||
*/
|
||||
public function findVersionByNumber(int $extensionId, int $version): ?TemplateLayoutExtensionVersion;
|
||||
|
||||
/**
|
||||
* 확장 ID 목록의 현재(최신) 버전 번호 맵 조회
|
||||
|
||||
@@ -138,12 +138,16 @@ interface LayoutRepositoryInterface
|
||||
public function updateContent(int $id, array $content, int $newLockVersion): TemplateLayout;
|
||||
|
||||
/**
|
||||
* 특정 레이아웃의 모든 버전 조회
|
||||
* 특정 레이아웃의 최근 버전 목록 조회 (최신순)
|
||||
*
|
||||
* 버전 행은 저장할 때마다 쌓이고 정리되지 않으므로 조회 건수에 상한이 있습니다.
|
||||
* 목록이 쓰지 않는 `content`(버전마다 레이아웃 본문 사본)는 조회하지 않습니다.
|
||||
*
|
||||
* @param int $layoutId 레이아웃 ID
|
||||
* @param int $limit 조회할 최대 버전 수
|
||||
* @return Collection 버전 컬렉션
|
||||
*/
|
||||
public function getVersionsByLayoutId(int $layoutId): Collection;
|
||||
public function getVersionsByLayoutId(int $layoutId, int $limit = 100): Collection;
|
||||
|
||||
/**
|
||||
* 특정 버전 조회
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Exceptions\ConcurrentModificationException;
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Requests\Admin\LayoutVersionListRequest;
|
||||
use App\Http\Requests\Layout\StoreLayoutPreviewRequest;
|
||||
use App\Http\Requests\Layout\UpdateLayoutContentRequest;
|
||||
use App\Http\Resources\LayoutListResource;
|
||||
@@ -148,11 +149,12 @@ class LayoutController extends AdminBaseController
|
||||
/**
|
||||
* 레이아웃의 모든 버전 목록 조회
|
||||
*
|
||||
* @param LayoutVersionListRequest $request 버전 목록 조회 요청 (limit)
|
||||
* @param string $templateName 템플릿 identifier
|
||||
* @param string $name 레이아웃 이름
|
||||
* @return JsonResponse 버전 목록 응답
|
||||
*/
|
||||
public function versions(string $templateName, string $name): JsonResponse
|
||||
public function versions(LayoutVersionListRequest $request, string $templateName, string $name): JsonResponse
|
||||
{
|
||||
$template = $this->templateService->findByIdentifier($templateName);
|
||||
|
||||
@@ -166,12 +168,19 @@ class LayoutController extends AdminBaseController
|
||||
return $this->notFound('common.not_found');
|
||||
}
|
||||
|
||||
$versions = $this->layoutService->getLayoutVersions($template->id, $name);
|
||||
|
||||
return $this->success(
|
||||
'common.success',
|
||||
LayoutVersionResource::collection($versions)
|
||||
$versions = $this->layoutService->getLayoutVersions(
|
||||
$template->id,
|
||||
$name,
|
||||
(int) ($request->validated()['limit'] ?? LayoutVersionListRequest::DEFAULT_LIMIT)
|
||||
);
|
||||
|
||||
// 목록은 경량 표현만 내려준다 — 분해된 본문은 버전 비교 diff 전용이라 단건 조회가 공급한다.
|
||||
$items = $versions
|
||||
->map(fn ($version) => (new LayoutVersionResource($version))->toListArray($request))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return $this->success('common.success', $items);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Exceptions\ConcurrentModificationException;
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Requests\Admin\LayoutVersionListRequest;
|
||||
use App\Http\Requests\Layout\StoreLayoutExtensionPreviewRequest;
|
||||
use App\Http\Requests\Layout\UpdateLayoutExtensionContentRequest;
|
||||
use App\Http\Resources\LayoutExtensionResource;
|
||||
@@ -58,7 +59,11 @@ class LayoutExtensionController extends AdminBaseController
|
||||
$this->layoutExtensionService->getExtensionHostLayouts($extension)
|
||||
);
|
||||
}
|
||||
$group['extensions'] = LayoutExtensionResource::collection($group['extensions']);
|
||||
// 목록은 경량 표현만 내려준다 — 편집 본문(content)은 단건 조회가 공급한다.
|
||||
$group['extensions'] = collect($group['extensions'])
|
||||
->map(fn ($extension) => (new LayoutExtensionResource($extension))->toListArray(request()))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return $group;
|
||||
}, $groups);
|
||||
@@ -156,11 +161,12 @@ class LayoutExtensionController extends AdminBaseController
|
||||
/**
|
||||
* 레이아웃 확장의 모든 버전 목록 조회
|
||||
*
|
||||
* @param LayoutVersionListRequest $request 버전 목록 조회 요청 (limit)
|
||||
* @param string $templateName 템플릿 identifier
|
||||
* @param int $extensionId 확장 ID
|
||||
* @return JsonResponse 버전 목록 응답
|
||||
*/
|
||||
public function versions(string $templateName, int $extensionId): JsonResponse
|
||||
public function versions(LayoutVersionListRequest $request, string $templateName, int $extensionId): JsonResponse
|
||||
{
|
||||
$extension = $this->resolveExtension($templateName, $extensionId);
|
||||
|
||||
@@ -168,9 +174,18 @@ class LayoutExtensionController extends AdminBaseController
|
||||
return $this->notFound('common.not_found');
|
||||
}
|
||||
|
||||
$versions = $this->layoutExtensionService->getExtensionVersions($extensionId);
|
||||
$versions = $this->layoutExtensionService->getExtensionVersions(
|
||||
$extensionId,
|
||||
(int) ($request->validated()['limit'] ?? LayoutVersionListRequest::DEFAULT_LIMIT)
|
||||
);
|
||||
|
||||
return $this->success('common.success', LayoutExtensionVersionResource::collection($versions));
|
||||
// 목록은 경량 표현만 내려준다 — 확장 본문은 버전 비교 diff 전용이라 단건 조회가 공급한다.
|
||||
$items = $versions
|
||||
->map(fn ($version) => (new LayoutExtensionVersionResource($version))->toListArray($request))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return $this->success('common.success', $items);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Extension\HookManager;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* 레이아웃 버전 목록 조회 요청
|
||||
*
|
||||
* 버전 행은 저장할 때마다 쌓이고 정리(pruning)되지 않습니다. 상한이 없으면 오래 편집한
|
||||
* 레이아웃 하나가 수백~수천 건의 버전을 한 응답에 담게 되므로 조회 건수를 제한합니다.
|
||||
* 더 오래된 이력이 필요하면 `limit` 을 명시해 넓힐 수 있습니다.
|
||||
*/
|
||||
class LayoutVersionListRequest extends FormRequest
|
||||
{
|
||||
/** 기본 조회 건수 (미지정 시) */
|
||||
public const DEFAULT_LIMIT = 100;
|
||||
|
||||
/** 한 번에 조회 가능한 최대 건수 */
|
||||
public const MAX_LIMIT = 500;
|
||||
|
||||
/**
|
||||
* 권한 확인
|
||||
*
|
||||
* @return bool 인가 여부 (권한 체크는 미들웨어에 위임)
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 유효성 검사 규칙
|
||||
*
|
||||
* @return array<string, mixed> 검증 규칙
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'limit' => ['nullable', 'integer', 'min:1', 'max:'.self::MAX_LIMIT],
|
||||
];
|
||||
|
||||
// 모듈/플러그인이 목록 조회 파라미터를 확장할 수 있도록 훅을 노출한다
|
||||
return HookManager::applyFilters('core.layout_version.list_validation_rules', $rules, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 검증 오류 메시지 커스터마이징
|
||||
*
|
||||
* @return array<string, string> 오류 메시지
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'limit.integer' => __('validation.layout_version.limit.integer'),
|
||||
'limit.min' => __('validation.layout_version.limit.min'),
|
||||
'limit.max' => __('validation.layout_version.limit.max', ['max' => self::MAX_LIMIT]),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,11 @@ class NotificationDefinitionIndexRequest extends FormRequest
|
||||
'extension_type' => ['nullable', 'string', 'in:core,module,plugin'],
|
||||
'extension_identifier' => ['nullable', 'string', 'max:100'],
|
||||
'channel' => ['nullable', 'string', 'max:50'],
|
||||
// 목록에 실을 템플릿을 한 채널로 좁힌다. `channel` 과 다르다 — `channel` 은 정의 행
|
||||
// 자체를 거르지만(그 채널을 지원하지 않는 정의는 목록에서 사라진다), 이 값은 행은
|
||||
// 그대로 두고 각 행에 딸려오는 템플릿만 좁힌다. 화면이 한 번에 한 채널만 그리므로
|
||||
// 나머지 채널의 제목·본문은 전송할 이유가 없다.
|
||||
'template_channel' => ['nullable', 'string', 'max:50'],
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
'sort_by' => ['nullable', 'string', 'in:id,type,extension_type,is_active,created_at,updated_at'],
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Resources\Admin\Identity;
|
||||
|
||||
use App\Http\Resources\BaseApiResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\MissingValue;
|
||||
|
||||
/**
|
||||
* IDV 메시지 정의 리소스.
|
||||
@@ -60,9 +61,25 @@ class IdentityMessageDefinitionResource extends BaseApiResource
|
||||
'is_active' => (bool) $this->getValue('is_active'),
|
||||
'is_default' => (bool) $this->getValue('is_default'),
|
||||
'user_overrides' => $this->getValue('user_overrides'),
|
||||
'templates' => $this->relationLoaded('templates')
|
||||
? IdentityMessageTemplateResource::collection($this->templates)
|
||||
: null,
|
||||
// 목록은 대표 템플릿 1건만 로드하고(firstTemplate), 단건은 전체를 로드한다(templates).
|
||||
// 화면 계약은 양쪽 모두 `templates` 배열이라 여기서 하나로 합류시킨다 —
|
||||
// 로드된 쪽을 쓰고, 어느 쪽도 로드되지 않았으면 키 자체를 내보내지 않는다.
|
||||
//
|
||||
// 대표 템플릿 분기는 `whenLoaded` 로 쓸 수 없다. `whenLoaded($relation, $value)` 는
|
||||
// 관계가 로드됐어도 값이 null 이면 $value 를 평가하지 않고 그대로 null 을 돌려준다
|
||||
// (HasOne 이 0건이면 null). 그러면 "로드하지 않음"(null)과 "로드했더니 0건"(빈 배열)이
|
||||
// 같은 값이 되어 소비자가 둘을 구분할 수 없다 — 관계 로드 여부는 직접 판정한다.
|
||||
'templates' => $this->whenLoaded(
|
||||
'templates',
|
||||
fn () => IdentityMessageTemplateResource::collection($this->templates),
|
||||
fn () => $this->resource->relationLoaded('firstTemplate')
|
||||
? ($this->firstTemplate
|
||||
? IdentityMessageTemplateResource::collection(collect([$this->firstTemplate]))
|
||||
: [])
|
||||
: new MissingValue,
|
||||
),
|
||||
// 목록이 대표 1건만 싣기 때문에, "템플릿이 더 있는가" 는 배열 길이로 알 수 없다.
|
||||
'templates_count' => $this->whenHas('templates_count', fn () => (int) $this->templates_count),
|
||||
...$this->formatTimestamps(),
|
||||
...$this->resourceMeta($request),
|
||||
];
|
||||
|
||||
@@ -5,13 +5,16 @@ namespace App\Http\Resources;
|
||||
use App\Helpers\TimezoneHelper;
|
||||
use App\Http\Resources\Traits\HasAbilityCheck;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Http\Resources\MissingValue;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class BaseApiResource extends JsonResource
|
||||
{
|
||||
use HasAbilityCheck;
|
||||
|
||||
/**
|
||||
* 리소스를 배열로 변환합니다.
|
||||
*
|
||||
@@ -122,6 +125,76 @@ class BaseApiResource extends JsonResource
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건부 필드(`when()`)가 걸러지지 않은 배열에서 미충족 항목을 제거합니다.
|
||||
*
|
||||
* `toArray()` 결과는 Laravel 이 응답 직전에 `filter()` 로 `MissingValue` 를 걷어내지만,
|
||||
* `toListArray()` 처럼 **배열을 그대로 컨트롤러에 넘기는 경로**는 그 단계를 거치지 않는다.
|
||||
* 그대로 두면 조건 미충족 필드가 `"updated_at": {}` 같은 빈 객체로 응답에 실린다
|
||||
* (예: `UPDATED_AT = null` 인 모델의 타임스탬프).
|
||||
*
|
||||
* @param array<string, mixed> $data 직렬화 결과
|
||||
* @return array<string, mixed> 미충족 항목이 제거된 배열
|
||||
*/
|
||||
protected function withoutMissing(array $data): array
|
||||
{
|
||||
return array_filter($data, fn ($value) => ! $value instanceof MissingValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿼리에서 계산된 집계 별칭(`withCount`/`withSum` 등)이 실제로 실려 있는지 판정합니다.
|
||||
*
|
||||
* 값 검사(`!== null`, `> 0`)로는 판정할 수 없다. SUM 은 대상 0건에서 NULL 을 돌려주고
|
||||
* COUNT 는 0 을 돌려주므로, "집계하지 않았다" 와 "집계했더니 비어 있다" 가 구분되지 않는다.
|
||||
* 속성 키의 존재 여부만이 두 상황을 가른다.
|
||||
*
|
||||
* @param string $alias 집계 별칭 (예: `options_count`)
|
||||
* @return bool 집계가 실려 있는지 여부
|
||||
*/
|
||||
protected function hasAggregate(string $alias): bool
|
||||
{
|
||||
$model = $this->unwrapResource();
|
||||
|
||||
return $model instanceof Model && array_key_exists($alias, $model->getAttributes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 집계 별칭 값을 반환합니다. 집계가 실려 있지 않으면 기본값을 돌려줍니다.
|
||||
*
|
||||
* @param string $alias 집계 별칭
|
||||
* @param mixed $default 집계 부재 시 사용할 값
|
||||
* @return mixed 집계 값 또는 기본값
|
||||
*/
|
||||
protected function aggregate(string $alias, mixed $default = null): mixed
|
||||
{
|
||||
$model = $this->unwrapResource();
|
||||
|
||||
if (! $model instanceof Model || ! array_key_exists($alias, $model->getAttributes())) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $model->getAttributes()[$alias];
|
||||
}
|
||||
|
||||
/**
|
||||
* 리소스 래핑을 벗겨 원본 모델을 반환합니다.
|
||||
*
|
||||
* Resource 가 Resource 를 감싸는 경우(컬렉션 매핑 등)가 있어 한 겹만 벗기면 모델에
|
||||
* 닿지 못한다.
|
||||
*
|
||||
* @return mixed 래핑이 제거된 리소스 본체
|
||||
*/
|
||||
private function unwrapResource(): mixed
|
||||
{
|
||||
$model = $this->resource;
|
||||
|
||||
while ($model instanceof JsonResource) {
|
||||
$model = $model->resource;
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* datetime 값을 사용자 timezone으로 변환하여 ISO8601 문자열로 반환합니다.
|
||||
*
|
||||
|
||||
@@ -69,6 +69,56 @@ class LayoutExtensionResource extends BaseApiResource
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 목록용 경량 배열로 변환합니다.
|
||||
*
|
||||
* 확장 목록은 라우트 트리에 "어느 화면에 어떤 확장이 붙어 있는가" 를 그리는 데 쓰입니다.
|
||||
* 편집 본문(`content`)은 그 화면이 쓰지 않고 단건 조회가 공급하는데, 목록에 실리면 템플릿에
|
||||
* 등록된 **모든 확장의 JSON 전문**이 한 응답에 담깁니다.
|
||||
*
|
||||
* 수정 여부(`is_modified`)와 크기(`size`/`size_formatted`)는 본문에서 계산하므로 값은 그대로
|
||||
* 유지합니다 — 본문을 직렬화하지 않을 뿐, 계산은 서버에서 수행해 결과만 내려줍니다.
|
||||
* (목록 화면이 확장마다 크기를 표시하므로 빼면 전 행이 `0 B` 로 보인다.)
|
||||
*
|
||||
* @param Request $request 요청 객체
|
||||
* @return array<string, mixed> 목록용 직렬화 결과
|
||||
*/
|
||||
public function toListArray(Request $request): array
|
||||
{
|
||||
$content = $this->getValue('content', []);
|
||||
$contentJson = is_array($content) ? json_encode($content, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : '{}';
|
||||
|
||||
$extensionType = $this->getValue('extension_type');
|
||||
$sourceType = $this->getValue('source_type');
|
||||
|
||||
// 이 배열은 컨트롤러가 그대로 응답에 싣는다 — Laravel 의 MissingValue 제거 단계를 거치지
|
||||
// 않으므로 조건부 필드를 직접 걸러낸다.
|
||||
return $this->withoutMissing([
|
||||
'id' => $this->getValue('id'),
|
||||
'extension_type' => $extensionType?->value ?? $extensionType,
|
||||
'target_name' => $this->getValue('target_name'),
|
||||
'source_type' => $sourceType?->value ?? $sourceType,
|
||||
'source_identifier' => $this->getValue('source_identifier'),
|
||||
'source_label' => $this->getValue('source_label', $this->getValue('source_identifier')),
|
||||
'is_override' => (bool) $this->getValue(
|
||||
'is_override',
|
||||
($sourceType?->value ?? $sourceType) === 'template'
|
||||
),
|
||||
'priority' => $this->getValue('priority'),
|
||||
'is_active' => (bool) $this->getValue('is_active'),
|
||||
'is_modified' => $this->resolveIsModified($content),
|
||||
'size' => strlen($contentJson),
|
||||
'size_formatted' => $this->formatFileSize(strlen($contentJson)),
|
||||
'host_layouts' => $this->getValue('host_layouts', []),
|
||||
'current_version' => $this->getValue('current_version') !== null
|
||||
? (int) $this->getValue('current_version')
|
||||
: null,
|
||||
|
||||
...$this->formatTimestamps(),
|
||||
...$this->resourceMeta($request),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리소스별 권한 매핑을 반환합니다.
|
||||
*
|
||||
|
||||
@@ -39,6 +39,33 @@ class LayoutExtensionVersionResource extends BaseApiResource
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 목록용 경량 배열로 변환합니다.
|
||||
*
|
||||
* 버전 히스토리 패널이 쓰는 값은 버전 번호·변경 요약·저장자·저장 시각뿐입니다. 확장 본문
|
||||
* (`content`)은 버전 비교 diff 전용이라 단건 조회가 공급합니다. 목록에 실으면 **버전마다**
|
||||
* 확장 본문 사본이 실려, 저장을 반복한 확장일수록 목록 응답이 선형으로 커집니다.
|
||||
* (레이아웃 본체 LayoutVersionResource 와 동일 정책)
|
||||
*
|
||||
* @param Request $request 요청 객체
|
||||
* @return array<string, mixed> 목록용 직렬화 결과
|
||||
*/
|
||||
public function toListArray(Request $request): array
|
||||
{
|
||||
// 이 배열은 컨트롤러가 그대로 응답에 싣는다 — Laravel 의 MissingValue 제거 단계를 거치지
|
||||
// 않으므로 조건부 필드를 직접 걸러낸다 (이 모델은 UPDATED_AT 이 없어 updated_at 이 미충족).
|
||||
return $this->withoutMissing([
|
||||
'id' => $this->getValue('id'),
|
||||
'extension_id' => $this->getValue('extension_id'),
|
||||
'version' => $this->getValue('version'),
|
||||
'changes_summary' => $this->formatChangesSummary($this->getValue('changes_summary', [])),
|
||||
'created_by_name' => $this->resolveCreatorName(),
|
||||
|
||||
...$this->formatTimestamps(),
|
||||
...$this->resourceMeta($request),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장자 이름을 해석합니다.
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LayoutVersionResource extends BaseApiResource
|
||||
@@ -64,6 +65,36 @@ class LayoutVersionResource extends BaseApiResource
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 목록용 경량 배열로 변환합니다.
|
||||
*
|
||||
* 버전 히스토리 패널이 쓰는 값은 버전 번호·변경 요약·저장자·저장 시각뿐입니다
|
||||
* (`LayoutVersionSummary`). 분해된 본문(`components`/`data_sources`/`metadata`/`endpoint`)은
|
||||
* 버전 비교 diff 전용이라 단건 조회가 공급합니다. 목록에 실으면 **버전마다** 레이아웃 본문
|
||||
* 사본이 실려, 저장을 반복한 레이아웃일수록 목록 응답이 선형으로 커집니다.
|
||||
*
|
||||
* 종전 주석은 "목록에는 content 를 제외한다" 고 했지만 실제로 제외된 것은 `full_content`
|
||||
* 하나뿐이었습니다 — 분해된 키들이 그대로 남아 사실상 본문이 다 실렸습니다.
|
||||
*
|
||||
* @param Request $request 현재 요청
|
||||
* @return array<string, mixed> 목록용 직렬화 결과
|
||||
*/
|
||||
public function toListArray(Request $request): array
|
||||
{
|
||||
// 이 배열은 컨트롤러가 그대로 응답에 싣는다 — Laravel 의 MissingValue 제거 단계를 거치지
|
||||
// 않으므로 조건부 필드를 직접 걸러낸다 (이 모델은 UPDATED_AT 이 없어 updated_at 이 미충족).
|
||||
return $this->withoutMissing([
|
||||
'id' => $this->getValue('id'),
|
||||
'layout_id' => $this->getValue('layout_id'),
|
||||
'version' => $this->getValue('version'),
|
||||
'changes_summary' => $this->formatChangesSummary($this->getValue('changes_summary', [])),
|
||||
'created_by_name' => $this->resolveCreatorName(),
|
||||
|
||||
...$this->formatTimestamps(),
|
||||
...$this->resourceMeta($request),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장자 이름을 해석합니다.
|
||||
*
|
||||
@@ -77,7 +108,7 @@ class LayoutVersionResource extends BaseApiResource
|
||||
$creator = $this->whenLoaded('creator');
|
||||
|
||||
// whenLoaded 미로딩 시 MissingValue 반환 → 삼항으로 null 처리 (MissingValue 누출 방지).
|
||||
return $creator instanceof \App\Models\User ? $creator->name : null;
|
||||
return $creator instanceof User ? $creator->name : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,7 +38,7 @@ class MenuResource extends BaseApiResource
|
||||
|
||||
'children' => $this->whenLoaded('children', function () {
|
||||
return $this->children->map(function ($child) {
|
||||
return [
|
||||
$data = [
|
||||
'id' => $child->id,
|
||||
'name' => $child->name, // 다국어 객체 그대로 반환
|
||||
'slug' => $child->slug,
|
||||
@@ -49,14 +49,23 @@ class MenuResource extends BaseApiResource
|
||||
'parent_id' => $child->parent_id,
|
||||
'extension_type' => $child->extension_type?->value,
|
||||
'extension_identifier' => $child->extension_identifier,
|
||||
'roles' => $child->relationLoaded('roles') ? $child->roles->map(function ($role) {
|
||||
];
|
||||
|
||||
// 역할은 로드된 경우에만 싣는다. 미로드 시 `[]` 를 돌려주면 "역할 제한이
|
||||
// 없다" 는 사실이 아닌 단언이 되어, 화면이 그 값을 그대로 저장에 실어
|
||||
// 보내면 해당 메뉴의 역할 제한이 통째로 지워진다. 키 자체를 생략해
|
||||
// 소비 측이 "모른다" 를 구분할 수 있게 한다 (단건 조회로 보강).
|
||||
if ($child->relationLoaded('roles')) {
|
||||
$data['roles'] = $child->roles->map(function ($role) {
|
||||
return [
|
||||
'id' => $role->id,
|
||||
'name' => $role->name,
|
||||
'permission_type' => $role->pivot->permission_type ?? null,
|
||||
];
|
||||
}) : [],
|
||||
];
|
||||
})->values();
|
||||
}
|
||||
|
||||
return $data;
|
||||
})->sortBy('order')->values();
|
||||
}),
|
||||
|
||||
|
||||
@@ -20,12 +20,15 @@ class NotificationDefinitionResource extends BaseApiResource
|
||||
/**
|
||||
* 리소스를 배열로 변환합니다.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
// 컬렉션(NotificationDefinitionCollection)이 이 배열을 그대로 응답에 실어 Laravel 의
|
||||
// MissingValue 제거 단계를 거치지 않는다 — 미충족 조건부 필드를 직접 걸러낸다.
|
||||
// (걸러내지 않으면 `"templates": {}` 같은 빈 객체가 응답에 남는다)
|
||||
return $this->withoutMissing([
|
||||
'id' => $this->getValue('id'),
|
||||
'type' => $this->getValue('type'),
|
||||
'hook_prefix' => $this->getValue('hook_prefix'),
|
||||
@@ -38,11 +41,19 @@ class NotificationDefinitionResource extends BaseApiResource
|
||||
'hooks' => $this->getValue('hooks'),
|
||||
'is_active' => (bool) $this->getValue('is_active'),
|
||||
'is_default' => (bool) $this->getValue('is_default'),
|
||||
'templates' => $this->relationLoaded('templates')
|
||||
? NotificationTemplateResource::collection($this->templates)
|
||||
: null,
|
||||
'templates' => $this->whenLoaded(
|
||||
'templates',
|
||||
fn () => NotificationTemplateResource::collection($this->templates)
|
||||
),
|
||||
// 목록이 템플릿을 한 채널로 좁혀 싣기 때문에, "커스터마이즈된 템플릿이 있는가" 는
|
||||
// 배열을 훑어서는 알 수 없다 (다른 채널만 수정한 경우를 놓친다). 되돌리기 버튼
|
||||
// 노출 조건이 이 값에 의존하므로 집계로 내려보낸다.
|
||||
'has_customized_templates' => $this->whenHas(
|
||||
'customized_templates_count',
|
||||
fn () => (int) $this->customized_templates_count > 0
|
||||
),
|
||||
...$this->formatTimestamps(),
|
||||
...$this->resourceMeta($request),
|
||||
];
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,14 @@ class RoleResource extends BaseApiResource
|
||||
$this->resource->users_count !== null,
|
||||
fn () => $this->resource->users_count
|
||||
),
|
||||
// 목록은 권한 배열 3종을 싣지 않는다. 그 대신 "몇 개가 할당됐는가" 는 집계로 남긴다
|
||||
// — 목록 화면이 권한 규모를 보여주면서도 권한 트리를 전송하지 않게 하는 대체 경로다.
|
||||
// 값 검사(`!== null`)가 아니라 별칭 존재 여부로 판정한다: COUNT 는 0건에서도 0 을
|
||||
// 돌려주므로 "집계하지 않았다" 와 "집계했더니 0" 이 구분되지 않는다.
|
||||
'permissions_count' => $this->whenHas(
|
||||
'permissions_count',
|
||||
fn () => (int) $this->resource->permissions_count
|
||||
),
|
||||
'permission_ids' => $this->when(
|
||||
$this->resource->relationLoaded('permissions'),
|
||||
fn () => $this->resource->permissions->pluck('id')->toArray()
|
||||
|
||||
@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
/**
|
||||
* 본인인증 메시지 정의 모델.
|
||||
@@ -125,11 +126,31 @@ class IdentityMessageDefinition extends Model
|
||||
/**
|
||||
* 메시지 템플릿 관계.
|
||||
*
|
||||
* 등록 순서(기본키)로 정렬을 고정한다. 목록은 대표 1건만(`firstTemplate` = MIN(id)) 싣고
|
||||
* 단건은 전체를 싣는데, 화면 계약상 양쪽 모두 `templates[0]` 을 대표로 읽는다. 정렬이 없으면
|
||||
* 단건의 첫 항목이 DB 반환 순서(`(definition_id, channel)` 유니크 인덱스 순)를 타서, 목록에서
|
||||
* 본 제목과 다른 채널의 템플릿이 열린다.
|
||||
*
|
||||
* @return HasMany
|
||||
*/
|
||||
public function templates(): HasMany
|
||||
{
|
||||
return $this->hasMany(IdentityMessageTemplate::class, 'definition_id');
|
||||
return $this->hasMany(IdentityMessageTemplate::class, 'definition_id')->orderBy('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 대표 메시지 템플릿 관계 (가장 먼저 등록된 1건 = `templates` 정렬의 첫 항목).
|
||||
*
|
||||
* 정의 목록 화면은 대표 템플릿 1건의 제목/본문만 그린다. `templates` 를 통째로 eager load
|
||||
* 하면 한 페이지를 여는 것만으로 그 페이지 전 정의의 모든 채널 템플릿 본문이 응답에 실린다.
|
||||
* eager load 의 `limit(1)` 은 부모별이 아니라 배치 쿼리 전체에 걸리므로 쓸 수 없고,
|
||||
* 관계 자체를 "가장 오래된 1건" 으로 좁혀야 정의별 1건이 보장된다.
|
||||
*
|
||||
* @return HasOne 대표 메시지 템플릿과의 관계
|
||||
*/
|
||||
public function firstTemplate(): HasOne
|
||||
{
|
||||
return $this->hasOne(IdentityMessageTemplate::class, 'definition_id')->oldestOfMany();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,7 +53,13 @@ trait PaginatesWithDeferredJoin
|
||||
* @param int $perPage 페이지당 건수
|
||||
* @param int|null $page 현재 페이지 (null = 요청에서 해석)
|
||||
* @param array<int, string> $relations outer 전용 eager load 관계
|
||||
* @param array<int, string> $withCount outer 전용 관계 카운트
|
||||
* @param array<int|string, string|Closure> $withCount outer 전용 관계 카운트. `withCount()` 로
|
||||
* 그대로 forward 되므로 Laravel 의 두 형태를
|
||||
* 모두 받는다 — 목록 형태(`['options']`) 와
|
||||
* 제약 클로저를 붙인 연관 형태
|
||||
* (`['options as active_count' => fn ($q) => ...]`).
|
||||
* 별칭 없이 같은 관계를 두 번 세면 기본 별칭이
|
||||
* 충돌해 뒤엣것이 앞엣것을 덮으므로 `as` 로 구분한다
|
||||
* @param string $keyName 키 컬럼명 (기본 id)
|
||||
* @param int|null $total 전체 건수 (null = COUNT 수행, 값 지정 시 캐시 total 사용)
|
||||
* @param bool $simple true = COUNT 없이 다음 페이지 유무만 판정
|
||||
@@ -134,7 +140,7 @@ trait PaginatesWithDeferredJoin
|
||||
* @param array<int, array{column: string, direction: string}> $sort 정렬 스펙
|
||||
* @param array<int, mixed> $ids 이번 페이지의 키 값 목록
|
||||
* @param array<int, string> $relations eager load 관계
|
||||
* @param array<int, string> $withCount 관계 카운트
|
||||
* @param array<int|string, string|Closure> $withCount 관계 카운트 (목록/연관 두 형태 모두 허용)
|
||||
* @param string $keyName 키 컬럼명
|
||||
* @param bool $preserveIdOrder ID 순서 그대로 복원 여부
|
||||
* @param Closure|null $outerUsing outer 전용 조인/집계
|
||||
|
||||
@@ -151,7 +151,11 @@ class IdentityMessageDefinitionRepository implements IdentityMessageDefinitionRe
|
||||
*/
|
||||
public function getPaginated(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
||||
{
|
||||
$query = IdentityMessageDefinition::with('templates');
|
||||
// 목록은 대표 템플릿 1건만 싣는다 — 화면이 그리는 것은 `templates[0]` 의 제목/본문뿐이다.
|
||||
// `templates` 를 통째로 로드하면 한 페이지를 여는 것만으로 그 페이지 전 정의의 모든 채널
|
||||
// 템플릿 본문이 응답에 실린다. 전체 템플릿은 단건 조회(show)가 공급한다.
|
||||
$query = IdentityMessageDefinition::with('firstTemplate')
|
||||
->withCount('templates as templates_count');
|
||||
|
||||
if (! empty($filters['provider_id'])) {
|
||||
$query->where('provider_id', $filters['provider_id']);
|
||||
|
||||
@@ -48,20 +48,49 @@ class LayoutExtensionVersionRepository implements LayoutExtensionVersionReposito
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 확장의 모든 버전 조회 (최신순)
|
||||
* 특정 확장의 최근 버전 목록 조회 (최신순)
|
||||
*
|
||||
* @param int $extensionId 레이아웃 확장 ID
|
||||
* @param int $limit 조회할 최대 버전 수 (버전 행은 정리되지 않고 쌓이므로 상한 필수)
|
||||
* @return Collection 버전 컬렉션
|
||||
*/
|
||||
public function getVersions(int $extensionId): Collection
|
||||
public function getVersions(int $extensionId, int $limit = 100): Collection
|
||||
{
|
||||
// creator eager load — 버전 목록에 저장자 이름(created_by_name) 노출용 (N+1 회피,
|
||||
// 레이아웃 본체 getVersionsByLayoutId 와 동일 정책).
|
||||
//
|
||||
// 버전 행은 저장할 때마다 쌓이고 정리되지 않으므로 조회 건수에 상한을 둔다. 목록이 쓰지
|
||||
// 않는 `content`(버전마다 확장 본문 사본)도 조회하지 않는다 — 본문은 버전 비교 diff
|
||||
// 전용이며 단건 조회가 공급한다. (레이아웃 본체와 동일 정책)
|
||||
return $this->model
|
||||
->with('creator:id,name')
|
||||
->where('extension_id', $extensionId)
|
||||
->orderBy('version', 'desc')
|
||||
->get();
|
||||
->limit($limit)
|
||||
->get(['id', 'extension_id', 'version', 'changes_summary', 'created_by', 'created_at']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 확장의 특정 버전 번호 조회 (본문 포함)
|
||||
*
|
||||
* 목록(getVersions)에서 뺀 `content` 의 대체 경로다. 목록 조회를 재사용하면 두 가지가
|
||||
* 동시에 깨진다 — ① 목록이 조회하지 않는 `content` 가 빈 값이 되어 버전 비교 diff 가
|
||||
* "전 항목 삭제"로 표시되고 ② 목록 상한(기본 100건) 밖의 오래된 버전이 404 가 된다.
|
||||
* (레이아웃 본체 LayoutRepository::findVersionByNumber 와 동일 정책)
|
||||
*
|
||||
* @param int $extensionId 레이아웃 확장 ID
|
||||
* @param int $version 버전 번호
|
||||
* @return TemplateLayoutExtensionVersion|null 찾은 버전 모델 또는 null
|
||||
*/
|
||||
public function findVersionByNumber(int $extensionId, int $version): ?TemplateLayoutExtensionVersion
|
||||
{
|
||||
// creator eager load — 단건 버전 조회도 저장자 이름을 목록과 일관 노출.
|
||||
return $this->model
|
||||
->newQuery()
|
||||
->with('creator:id,name')
|
||||
->where('extension_id', $extensionId)
|
||||
->where('version', $version)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,12 +17,25 @@ class LayoutRepository implements LayoutRepositoryInterface
|
||||
/**
|
||||
* 특정 템플릿의 모든 레이아웃 조회
|
||||
*
|
||||
* 편집 본문(`content`)은 읽지 않는다. 이 메서드의 남은 소비처는 캐시 무효화
|
||||
* (`InvalidatesLayoutCache` 트레이트)뿐이고 그 경로는 캐시 키를 만드는 식별 컬럼만 쓰는데,
|
||||
* 템플릿 하나에 수십~수백 레이아웃이 있으면 본문 JSON 전량이 메모리에 올라온다.
|
||||
*
|
||||
* 목록 응답은 이 메서드가 아니라 {@see getListByTemplateId()} 가 공급하고(본문에서 파생되는
|
||||
* 설명·크기만 남기고 본문은 즉시 버린다), 본문 자체가 필요한 경로는 단건 조회
|
||||
* (`findByName`)가 공급한다.
|
||||
*
|
||||
* @param int $templateId 템플릿 ID
|
||||
* @return Collection 레이아웃 컬렉션
|
||||
*/
|
||||
public function getByTemplateId(int $templateId): Collection
|
||||
{
|
||||
return TemplateLayout::where('template_id', $templateId)
|
||||
->select([
|
||||
'id', 'template_id', 'name', 'original_content_hash', 'original_content_size',
|
||||
'extends', 'source_type', 'source_identifier', 'created_by', 'updated_by',
|
||||
'lock_version', 'created_at', 'updated_at',
|
||||
])
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}
|
||||
@@ -336,15 +349,24 @@ class LayoutRepository implements LayoutRepositoryInterface
|
||||
* 특정 레이아웃의 모든 버전 조회
|
||||
*
|
||||
* @param int $layoutId 레이아웃 ID
|
||||
* @param int $limit 조회할 최대 버전 수 (버전 행은 정리되지 않고 쌓이므로 상한 필수)
|
||||
* @return Collection 버전 컬렉션
|
||||
*/
|
||||
public function getVersionsByLayoutId(int $layoutId): Collection
|
||||
public function getVersionsByLayoutId(int $layoutId, int $limit = 100): Collection
|
||||
{
|
||||
// creator eager load — 버전 목록에 저장자 이름(created_by_name) 노출용 (N+1 회피).
|
||||
//
|
||||
// 목록이 쓰는 컬럼만 조회한다. `content` 는 버전마다 레이아웃 본문 사본이라, 상한이 있어도
|
||||
// 100건이면 본문 100벌을 읽는다. 본문은 버전 비교 diff 전용이며 단건 조회가 공급한다.
|
||||
// 정렬 기준은 `version` — 레이아웃 안에서 유일하다(uk_layout_version). 종전의
|
||||
// `latest()`(created_at)는 같은 초에 저장된 버전들의 순서를 정하지 못해, 상한이 걸린
|
||||
// 지금은 "어느 100건이 오는가" 까지 실행마다 달라질 수 있다.
|
||||
return TemplateLayoutVersion::with('creator:id,name')
|
||||
->where('layout_id', $layoutId)
|
||||
->latest()
|
||||
->get();
|
||||
->orderBy('version', 'desc')
|
||||
->limit($limit)
|
||||
// 이 테이블에는 updated_at 이 없다 (버전 행은 불변 스냅샷이라 created_at 만 둔다)
|
||||
->get(['id', 'layout_id', 'version', 'changes_summary', 'created_by', 'created_at']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,9 +420,9 @@ class LayoutRepository implements LayoutRepositoryInterface
|
||||
* 특정 템플릿의 모든 레이아웃 이름 조회
|
||||
*
|
||||
* @param int $templateId 템플릿 ID
|
||||
* @return \Illuminate\Support\Collection<int, string> 레이아웃 이름 컬렉션
|
||||
* @return SupportCollection<int, string> 레이아웃 이름 컬렉션
|
||||
*/
|
||||
public function getLayoutNamesByTemplateId(int $templateId): \Illuminate\Support\Collection
|
||||
public function getLayoutNamesByTemplateId(int $templateId): SupportCollection
|
||||
{
|
||||
return TemplateLayout::where('template_id', $templateId)
|
||||
->pluck('name')
|
||||
|
||||
@@ -95,8 +95,13 @@ class MenuRepository implements MenuRepositoryInterface
|
||||
if ($user) {
|
||||
$query->accessibleBy($user);
|
||||
}
|
||||
$query->with('roles')
|
||||
->orderBy('order');
|
||||
// 하위 메뉴의 역할은 목록에서 로드하지 않는다 — 화면이 쓰는 것은 하위 메뉴의
|
||||
// 개수와 표시 정보뿐이고, 역할 배지는 선택한(상위) 메뉴에만 나온다. 로드하면
|
||||
// 상위 메뉴마다 하위 전체의 역할 피벗까지 함께 실려 응답이 배가 된다.
|
||||
// Resource 는 로드되지 않은 하위 메뉴의 `roles` 키를 **생략**한다(빈 배열이 아니다)
|
||||
// — 빈 배열은 "역할 제한 없음" 이라는 사실 아닌 단언이 되어, 화면이 그 값을 그대로
|
||||
// 저장 요청에 실으면 역할 제한이 통째로 해제된다.
|
||||
$query->orderBy('order');
|
||||
}])
|
||||
->orderBy('order')
|
||||
->get();
|
||||
@@ -470,7 +475,12 @@ class MenuRepository implements MenuRepositoryInterface
|
||||
$childQuery->where('is_active', $filters['is_active']);
|
||||
}
|
||||
|
||||
$childQuery->with('roles')->orderBy('order');
|
||||
// 하위 메뉴의 역할은 목록에서 로드하지 않는다 — 화면이 쓰는 것은 하위 메뉴의
|
||||
// 개수와 표시 정보뿐이고, 역할 배지는 선택한(상위) 메뉴에만 나온다. 로드하면
|
||||
// 상위 메뉴마다 하위 전체의 역할 피벗까지 함께 실려 응답이 배가 된다.
|
||||
// 접근 제어(accessibleBy)는 whereHas 로 결과 집합을 좁히므로 그대로 유지된다.
|
||||
// Resource 는 로드되지 않은 하위 메뉴의 `roles` 키를 생략한다 — 위 메서드와 동일.
|
||||
$childQuery->orderBy('order');
|
||||
}])->get();
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +131,28 @@ class NotificationDefinitionRepository implements NotificationDefinitionReposito
|
||||
*/
|
||||
public function getPaginated(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
||||
{
|
||||
$query = NotificationDefinition::with('templates');
|
||||
// 목록은 템플릿 본문을 싣지 않는다.
|
||||
//
|
||||
// 좁히지 않으면 한 페이지를 여는 것만으로 그 페이지 전 정의의 **모든 채널** 제목·본문이
|
||||
// 응답에 실린다(정의 하나당 채널 × 로케일). 화면은 한 번에 한 채널만 그리므로 그 채널을
|
||||
// `template_channel` 로 지정해 요청한다 — 지정하지 않은 호출자는 템플릿을 받지 않고,
|
||||
// 필요하면 단건 조회(`GET .../notification-definitions/{id}`)가 전 채널을 제공한다.
|
||||
$templateChannel = $filters['template_channel'] ?? null;
|
||||
|
||||
$query = NotificationDefinition::query()
|
||||
// "되돌리기" 버튼 노출 조건은 채널을 가리지 않고 "커스터마이즈된 템플릿이 하나라도
|
||||
// 있는가" 다. 템플릿을 한 채널로 좁히면 배열만으로는 알 수 없으므로 집계로 낸다.
|
||||
->withCount([
|
||||
'templates as customized_templates_count' => function ($q) {
|
||||
$q->where('is_default', false);
|
||||
},
|
||||
]);
|
||||
|
||||
if (! empty($templateChannel)) {
|
||||
$query->with([
|
||||
'templates' => fn ($q) => $q->where('channel', $templateChannel),
|
||||
]);
|
||||
}
|
||||
|
||||
if (! empty($filters['extension_type'])) {
|
||||
$query->where('extension_type', $filters['extension_type']);
|
||||
|
||||
@@ -195,9 +195,19 @@ class RoleRepository implements RoleRepositoryInterface
|
||||
*/
|
||||
public function getPaginated(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
||||
{
|
||||
// 권한 관계는 목록에서 로드하지 않는다. 목록 화면이 쓰는 것은 이름·설명·사용자 수·활성
|
||||
// 여부뿐인데, 로드하면 Resource 가 같은 권한 집합을 `permissions`(계층 트리)·
|
||||
// `permission_ids`·`permission_values` 세 형태로 중복 직렬화하고, 계층 트리를 만드느라
|
||||
// 행마다 상위/카테고리 권한을 다시 조회한다(행 수만큼 추가 쿼리).
|
||||
//
|
||||
// Resource 의 `relationLoaded('permissions')` 가드는 그대로 두면 여기서 로드하지 않는 것만으로
|
||||
// 세 필드가 함께 빠진다 — 권한 편집은 단건 조회(`GET /admin/roles/{id}`)가 공급한다.
|
||||
//
|
||||
// 권한 **개수**는 집계로 남긴다. 목록에서 뺀 값의 대체 경로 — 권한 트리를 전송하지 않고도
|
||||
// "이 역할에 권한이 몇 개 걸려 있는가" 를 화면이 보여줄 수 있다. 값 검사가 아니라 별칭
|
||||
// 존재 여부로 판정하므로(RoleResource) 0건인 역할도 0 으로 정확히 표시된다.
|
||||
$query = Role::query()
|
||||
->withCount('users')
|
||||
->with('permissions');
|
||||
->withCount(['users', 'permissions']);
|
||||
|
||||
// 검색 필터
|
||||
if (! empty($filters['search'])) {
|
||||
|
||||
@@ -1661,21 +1661,24 @@ class LayoutExtensionService
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 확장의 모든 버전을 조회합니다.
|
||||
* 특정 확장의 최근 버전 목록을 조회합니다 (최신순).
|
||||
*
|
||||
* 버전 행은 저장할 때마다 쌓이고 정리되지 않으므로 조회 건수에 상한이 있습니다.
|
||||
*
|
||||
* @param int $extensionId 확장 ID
|
||||
* @param int $limit 조회할 최대 버전 수 (미지정 시 100)
|
||||
* @return EloquentCollection<int, TemplateLayoutExtensionVersion>
|
||||
*
|
||||
* @throws ModelNotFoundException 확장을 찾을 수 없는 경우
|
||||
*/
|
||||
public function getExtensionVersions(int $extensionId): EloquentCollection
|
||||
public function getExtensionVersions(int $extensionId, int $limit = 100): EloquentCollection
|
||||
{
|
||||
HookManager::doAction('core.layout_extension.before_versions_index', $extensionId);
|
||||
|
||||
// 확장 존재 검증
|
||||
$this->getExtensionById($extensionId);
|
||||
|
||||
$versions = $this->versionRepository->getVersions($extensionId);
|
||||
$versions = $this->versionRepository->getVersions($extensionId, $limit);
|
||||
|
||||
return HookManager::applyFilters('core.layout_extension.after_versions_index', $versions, $extensionId);
|
||||
}
|
||||
@@ -1696,8 +1699,10 @@ class LayoutExtensionService
|
||||
// 확장 존재 검증
|
||||
$this->getExtensionById($extensionId);
|
||||
|
||||
$extensionVersion = $this->versionRepository->getVersions($extensionId)
|
||||
->firstWhere('version', $version);
|
||||
// 목록용 getVersions() 를 재사용하지 않는다 — 목록은 경량 조회라 확장 본문(content)을
|
||||
// 담지 않고 건수 상한도 있어, 재사용하면 버전 비교가 "전 항목 삭제"로 표시되고 상한 밖의
|
||||
// 오래된 버전이 404 가 된다.
|
||||
$extensionVersion = $this->versionRepository->findVersionByNumber($extensionId, $version);
|
||||
|
||||
if (! $extensionVersion) {
|
||||
throw new ModelNotFoundException(
|
||||
|
||||
@@ -1479,13 +1479,17 @@ class LayoutService
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 레이아웃의 모든 버전 조회
|
||||
* 특정 레이아웃의 최근 버전 목록 조회 (최신순)
|
||||
*
|
||||
* 버전 행은 저장할 때마다 쌓이고 정리되지 않으므로 조회 건수에 상한을 둡니다.
|
||||
* 더 오래된 이력이 필요하면 호출자가 `$limit` 을 넓힙니다.
|
||||
*
|
||||
* @param int $templateId 대상 템플릿 ID
|
||||
* @param string $name 레이아웃 이름
|
||||
* @param int $limit 조회할 최대 버전 수
|
||||
* @return Collection 버전 컬렉션
|
||||
*/
|
||||
public function getLayoutVersions(int $templateId, string $name)
|
||||
public function getLayoutVersions(int $templateId, string $name, int $limit = 100)
|
||||
{
|
||||
// Before 훅 - 버전 목록 조회 전
|
||||
HookManager::doAction('core.layout.before_versions_index', $templateId, $name);
|
||||
@@ -1500,7 +1504,7 @@ class LayoutService
|
||||
}
|
||||
|
||||
// 버전 목록 조회
|
||||
$versions = $this->layoutRepository->getVersionsByLayoutId($layout->id);
|
||||
$versions = $this->layoutRepository->getVersionsByLayoutId($layout->id, $limit);
|
||||
|
||||
// After 훅 - 버전 목록 조회 후
|
||||
HookManager::doAction('core.layout.after_versions_index', $versions, $templateId, $name);
|
||||
|
||||
@@ -29,6 +29,14 @@ class ApiDocScaffolder
|
||||
*/
|
||||
private const LIST_EXAMPLE_LIMIT = 2;
|
||||
|
||||
/**
|
||||
* @var int 응답 예시 한 건의 최대 크기 (pretty JSON 기준 바이트)
|
||||
*
|
||||
* 목록 절단(`data.data[]`)만으로는 카탈로그·스펙처럼 목록이 아닌 큰 응답을 막지 못한다.
|
||||
* 상한을 넘으면 절단 강도를 단계적으로 올려 이 크기 이하로 줄인다.
|
||||
*/
|
||||
private const EXAMPLE_MAX_BYTES = 8192;
|
||||
|
||||
/**
|
||||
* @var string 요청 예시의 토큰 마스킹 placeholder
|
||||
*
|
||||
@@ -792,7 +800,10 @@ class ApiDocScaffolder
|
||||
// 응답 body 내부의 절대 URL(페이지네이터 링크·콜백 URL 등)에 실측 기준 호스트가
|
||||
// 그대로 직렬화돼 들어올 수 있으므로 공개 문서용 placeholder 호스트로 마스킹한다.
|
||||
$masked = $this->maskResponseHost($sanitized, $probeMeta['base_url'] ?? null);
|
||||
$json = json_encode($masked, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
// 목록 절단은 페이지네이션 형태(`data.data[]`)만 다룬다. 카탈로그·스펙처럼 목록이 아닌
|
||||
// 큰 응답은 그대로 두면 문서 한 파일이 수 MB 로 불어난다 — 전체 크기 상한을 별도로 건다.
|
||||
$capped = $this->capExampleSize($masked);
|
||||
$json = json_encode($capped, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
return "```http\nHTTP/1.1 {$status}\n```\n\n```json\n".(string) $json."\n```";
|
||||
}
|
||||
@@ -911,6 +922,101 @@ class ApiDocScaffolder
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 응답 예시 전체 크기를 상한 이하로 줄입니다.
|
||||
*
|
||||
* `truncateListBody()` 는 페이지네이션 형태(`data.data[]`)만 절단하므로, 편집기 스펙·컴포넌트
|
||||
* 카탈로그·라우트 목록처럼 목록이 아닌 큰 응답은 통째로 직렬화된다. 그대로 두면 문서 한 파일이
|
||||
* 수 MB 가 되어 리뷰·git·에디터가 모두 망가진다(#518 에서 templates.md 가 115KB → 8.65MB).
|
||||
*
|
||||
* 절단 강도를 단계적으로 올리며 상한 이하가 될 때까지 재시도하고, 그래도 넘으면 최상위 키만
|
||||
* 남긴 요약으로 대체한다. 어느 경우에도 "무엇이 잘렸는지" 를 문서에 남긴다.
|
||||
*
|
||||
* @param array<string, mixed> $body 마스킹까지 끝난 응답 body
|
||||
* @return array<string, mixed> 상한 이하로 절단된 body
|
||||
*/
|
||||
private function capExampleSize(array $body): array
|
||||
{
|
||||
if ($this->encodedSize($body) <= self::EXAMPLE_MAX_BYTES) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
// (배열 최대 항목 수, 문자열 최대 길이) 를 점점 조인다.
|
||||
foreach ([[5, 400], [3, 200], [1, 80]] as [$maxItems, $maxChars]) {
|
||||
$reduced = $this->shrinkNode($body, $maxItems, $maxChars);
|
||||
|
||||
if ($this->encodedSize($reduced) <= self::EXAMPLE_MAX_BYTES) {
|
||||
return $reduced;
|
||||
}
|
||||
}
|
||||
|
||||
// 여기까지 와도 상한을 넘으면 구조만 남긴다.
|
||||
$originalSize = $this->encodedSize($body);
|
||||
$data = $body['data'] ?? null;
|
||||
$keys = is_array($data) ? array_slice(array_keys($data), 0, 20) : [];
|
||||
|
||||
$body['data'] = [
|
||||
'// 응답이 너무 커서 예시로 싣지 않습니다 (약 '.number_format($originalSize).' bytes)',
|
||||
'// 최상위 키: '.($keys === [] ? '(스칼라 또는 빈 응답)' : implode(', ', $keys)),
|
||||
];
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 노드를 재귀적으로 축소합니다. 배열은 항목 수를, 문자열은 길이를 자릅니다.
|
||||
*
|
||||
* @param mixed $node 축소 대상
|
||||
* @param int $maxItems 배열/객체에서 남길 최대 항목 수
|
||||
* @param int $maxChars 문자열 최대 길이
|
||||
* @return mixed 축소된 노드
|
||||
*/
|
||||
private function shrinkNode(mixed $node, int $maxItems, int $maxChars): mixed
|
||||
{
|
||||
if (is_string($node)) {
|
||||
return mb_strlen($node) > $maxChars
|
||||
? mb_substr($node, 0, $maxChars).'… (생략)'
|
||||
: $node;
|
||||
}
|
||||
|
||||
if (! is_array($node)) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
$isList = array_is_list($node);
|
||||
$total = count($node);
|
||||
$sliced = array_slice($node, 0, $maxItems, ! $isList);
|
||||
|
||||
$result = [];
|
||||
foreach ($sliced as $key => $value) {
|
||||
$result[$key] = $this->shrinkNode($value, $maxItems, $maxChars);
|
||||
}
|
||||
|
||||
if ($total > $maxItems) {
|
||||
$omitted = $total - $maxItems;
|
||||
if ($isList) {
|
||||
$result[] = "... (총 {$total}건 중 {$maxItems}건 표시)";
|
||||
} else {
|
||||
$result['...'] = "({$omitted}개 키 생략, 총 {$total}개)";
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 노드를 문서에 실릴 형태로 인코딩했을 때의 바이트 수를 반환합니다.
|
||||
*
|
||||
* @param mixed $node 측정 대상
|
||||
* @return int 바이트 수 (인코딩 실패 시 상한 초과로 간주)
|
||||
*/
|
||||
private function encodedSize(mixed $node): int
|
||||
{
|
||||
$json = json_encode($node, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
return $json === false ? PHP_INT_MAX : strlen($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 셀 안에서 안전하도록 파이프/개행을 이스케이프합니다.
|
||||
*
|
||||
@@ -941,13 +1047,393 @@ class ApiDocScaffolder
|
||||
|
||||
foreach ($sections as $i => $section) {
|
||||
$key = $sectionKeys[$i];
|
||||
$preserved = $this->extractHumanProse($existing, $key);
|
||||
$merged .= $this->applyPreservedProse($section, $preserved)."\n";
|
||||
// 아직 생성 블록이 없던(사람이 직접 쓴) 섹션은 헤딩으로 찾는다. 키로만 찾으면
|
||||
// 최초 생성 회차에 그 섹션의 서술·표 설명이 통째로 사라진다.
|
||||
$legacy = $this->extractLegacySection($existing, $section, $key);
|
||||
|
||||
$preserved = $this->extractHumanProse($existing, $key)
|
||||
?? ($legacy === null ? null : $this->extractLegacyProse($legacy));
|
||||
$withProse = $this->applyPreservedProse($section, $preserved);
|
||||
// 순서 주의: 표 자체를 되살린 뒤에 셀 설명을 맞춘다.
|
||||
$withFields = $this->restoreEmptyProbeTable($withProse, $existing, $key, $legacy);
|
||||
$withTables = $this->restoreTableDescriptions($withFields, $existing, $key, $legacy);
|
||||
$merged .= $this->restoreErrorSection($withTables, $existing, $key, $legacy)."\n";
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* 새 섹션의 TODO 설명 셀을 기존 문서의 사람 서술로 되돌립니다.
|
||||
*
|
||||
* `@generated` 블록의 파라미터/응답 필드 표는 재생성 때마다 통째로 다시 조립되므로,
|
||||
* 정적 추출로 재현할 수 없는 도메인 설명(예: `no_category: 카테고리 미지정 상품만 필터`)이
|
||||
* TODO 스텁으로 되돌아간다. 사람이 채운 설명이 재생성 한 번에 사라지는 것은 문서 손실이므로,
|
||||
* 행 이름이 같고 새 셀이 TODO 스텁일 때만 기존 설명을 이어받는다.
|
||||
*
|
||||
* 기존 셀에 설명이 있으면 항상 그쪽을 쓴다. 도구가 만드는 설명은 필드명에서 유추한 일반
|
||||
* 문구(예: `대상의 이름/명칭`)라 사람이 적은 도메인 사실(예: `플러그인 이름 (다국어 JSON)`,
|
||||
* `desc 는 주문별 가장 늦은 발송일`)보다 언제나 정보량이 적다. 문서 헤더가 약속한
|
||||
* "사람이 작성한 설명은 보존됩니다" 를 표 셀에도 적용한다. 사실이 바뀌면 사람이 그 셀을
|
||||
* 고치거나 비우고, 비운 셀은 다음 재생성이 채운다.
|
||||
*
|
||||
* @param string $section 새로 생성된 섹션
|
||||
* @param string $existing 기존 문서 전체
|
||||
* @param string $key 라우트명(생성 블록 키)
|
||||
* @param string|null $legacy 생성 블록이 없던 사람 작성 섹션 원문 (헤딩 매칭 폴백)
|
||||
* @return string 설명이 복원된 섹션
|
||||
*/
|
||||
private function restoreTableDescriptions(string $section, string $existing, string $key, ?string $legacy = null): string
|
||||
{
|
||||
$oldBlock = $this->extractGeneratedBlockBody($existing, $key) ?? $legacy;
|
||||
if ($oldBlock === null) {
|
||||
return $section;
|
||||
}
|
||||
|
||||
$oldDescriptions = $this->collectRowDescriptions($oldBlock);
|
||||
if ($oldDescriptions === []) {
|
||||
return $section;
|
||||
}
|
||||
|
||||
$lines = explode("\n", $section);
|
||||
foreach ($lines as $index => $line) {
|
||||
if (! str_starts_with(trim($line), '|')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cells = $this->splitRowCells($line);
|
||||
if (count($cells) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lastIndex = count($cells) - 1;
|
||||
$rowKey = $this->rowKey($cells);
|
||||
if ($rowKey === null || ! isset($oldDescriptions[$rowKey])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cells[$lastIndex] = ' '.$oldDescriptions[$rowKey].' ';
|
||||
$lines[$index] = '|'.implode('|', $cells).'|';
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* 이번 실측이 빈 응답이라 필드 표가 사라졌으면 기존 표를 되살립니다.
|
||||
*
|
||||
* 실측 대상 데이터가 없는 상태로 재생성하면 응답 필드 표가 통째로
|
||||
* `<!-- 실측 응답에 필드 없음 ... -->` 한 줄로 대체된다. 이미 문서화돼 있던 필드 목록이
|
||||
* "이번에 데이터가 없었다" 는 이유로 사라지는 것은 사실의 후퇴이므로, 기존 표가 있으면 유지한다
|
||||
* (#518 에서 reviews.md 한 파일에서만 388줄이 이렇게 사라졌다).
|
||||
*
|
||||
* @param string $section 새로 생성된 섹션
|
||||
* @param string $existing 기존 문서 전체
|
||||
* @param string $key 라우트명(생성 블록 키)
|
||||
* @param string|null $legacy 생성 블록이 없던 사람 작성 섹션 원문
|
||||
* @return string 필드 표가 복원된 섹션
|
||||
*/
|
||||
private function restoreEmptyProbeTable(string $section, string $existing, string $key, ?string $legacy = null): string
|
||||
{
|
||||
$newFields = $this->sliceFieldSection($section);
|
||||
|
||||
// 이번 실측이 필드를 확보했으면(표가 있으면) 그대로 둔다.
|
||||
if ($newFields === null || ! str_contains($newFields, '실측 응답에 필드 없음')) {
|
||||
return $section;
|
||||
}
|
||||
|
||||
$oldBlock = $this->extractGeneratedBlockBody($existing, $key) ?? $legacy;
|
||||
if ($oldBlock === null) {
|
||||
return $section;
|
||||
}
|
||||
|
||||
$oldFields = $this->sliceFieldSection($oldBlock);
|
||||
if ($oldFields === null || ! str_contains($oldFields, '| --- |')) {
|
||||
return $section;
|
||||
}
|
||||
|
||||
return str_replace($newFields, $oldFields, $section);
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션에서 `**응답 필드**` 이후 ~ 다음 구획 전까지의 본문을 잘라냅니다.
|
||||
*
|
||||
* @param string $text 섹션 또는 생성 블록 본문
|
||||
* @return string|null 응답 필드 구간 본문 (없으면 null)
|
||||
*/
|
||||
private function sliceFieldSection(string $text): ?string
|
||||
{
|
||||
$pos = strpos($text, '**응답 필드**');
|
||||
if ($pos === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rest = substr($text, $pos);
|
||||
$lineEnd = strpos($rest, "\n");
|
||||
if ($lineEnd === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$body = substr($rest, $lineEnd);
|
||||
$at = strpos($body, "\n**");
|
||||
$end = $at === false ? strlen($body) : $at;
|
||||
|
||||
$slice = trim(substr($body, 0, $end));
|
||||
|
||||
return $slice === '' ? null : $slice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 응답 구간이 일반 스텁으로 되돌아가면 기존의 사람 서술을 되살립니다.
|
||||
*
|
||||
* `errorTable()` 은 라우트 메타만 보고 "대표 에러 없음 (공개 조회)" 같은 일반 문구를 만든다.
|
||||
* 사람이 도메인 특이 에러(예: "활성 에셋이 없으면 빈 200 응답")를 적어 뒀다면 재생성 한 번에
|
||||
* 그 사실이 사라지므로, 새 문구가 TODO 를 포함한 스텁일 때만 기존 구간을 이어받는다.
|
||||
*
|
||||
* @param string $section 새로 생성된 섹션
|
||||
* @param string $existing 기존 문서 전체
|
||||
* @param string $key 라우트명(생성 블록 키)
|
||||
* @param string|null $legacy 생성 블록이 없던 사람 작성 섹션 원문
|
||||
* @return string 에러 구간이 복원된 섹션
|
||||
*/
|
||||
private function restoreErrorSection(string $section, string $existing, string $key, ?string $legacy = null): string
|
||||
{
|
||||
$newError = $this->sliceErrorSection($section);
|
||||
|
||||
// 새 문구가 스텁이 아니면(=도구가 실제 에러를 뽑았으면) 그대로 둔다.
|
||||
if ($newError === null || ! str_contains($newError, '<!-- TODO')) {
|
||||
return $section;
|
||||
}
|
||||
|
||||
$oldBlock = $this->extractGeneratedBlockBody($existing, $key) ?? $legacy;
|
||||
if ($oldBlock === null) {
|
||||
return $section;
|
||||
}
|
||||
|
||||
$oldError = $this->sliceErrorSection($oldBlock);
|
||||
if ($oldError === null || $oldError === '' || str_contains($oldError, '<!-- TODO')) {
|
||||
return $section;
|
||||
}
|
||||
|
||||
return str_replace($newError, $oldError, $section);
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션에서 `**에러 응답**` 이후 ~ 다음 구획 전까지의 본문을 잘라냅니다.
|
||||
*
|
||||
* @param string $text 섹션 또는 생성 블록 본문
|
||||
* @return string|null 에러 구간 본문 (없으면 null)
|
||||
*/
|
||||
private function sliceErrorSection(string $text): ?string
|
||||
{
|
||||
$pos = strpos($text, '**에러 응답**');
|
||||
if ($pos === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rest = substr($text, $pos + strlen('**에러 응답**'));
|
||||
|
||||
// 다음 구획: 생성 블록 종료 마커 · 다음 굵은 헤딩 · 다음 ### 헤딩
|
||||
$stops = [];
|
||||
foreach ([self::GEN_END, "\n**", "\n### "] as $marker) {
|
||||
$at = strpos($rest, $marker);
|
||||
if ($at !== false) {
|
||||
$stops[] = $at;
|
||||
}
|
||||
}
|
||||
|
||||
$end = $stops === [] ? strlen($rest) : min($stops);
|
||||
|
||||
return trim(substr($rest, 0, $end));
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성 블록 마커가 없던 사람 작성 섹션을 헤딩으로 찾아 원문을 반환합니다.
|
||||
*
|
||||
* 문서가 수기 작성 → 생성 관리로 넘어가는 최초 회차에는 기존 섹션에 `@generated` 마커가
|
||||
* 없다. 키로만 찾으면 그 섹션의 표 설명과 서술이 통째로 사라지므로 헤딩(`### METHOD /uri`)
|
||||
* 으로 한 번 더 찾는다. 이미 키로 찾을 수 있으면 이 폴백은 쓰지 않는다.
|
||||
*
|
||||
* @param string $existing 기존 문서 전체
|
||||
* @param string $section 새로 생성된 섹션 (첫 줄이 헤딩)
|
||||
* @param string $key 라우트명(생성 블록 키)
|
||||
* @return string|null 사람 작성 섹션 원문 (없으면 null)
|
||||
*/
|
||||
private function extractLegacySection(string $existing, string $section, string $key): ?string
|
||||
{
|
||||
if (str_contains($existing, self::GEN_START.$key.' -->')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$heading = strtok($section, "\n");
|
||||
if (! is_string($heading) || ! str_starts_with($heading, '### ')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$startPos = strpos($existing, "\n".$heading."\n");
|
||||
if ($startPos === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rest = substr($existing, $startPos + 1);
|
||||
$nextHeading = preg_match('/\n### /', $rest, $m, PREG_OFFSET_CAPTURE)
|
||||
? $m[0][1]
|
||||
: strlen($rest);
|
||||
|
||||
return substr($rest, 0, $nextHeading);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사람 작성 섹션 원문에서 `**설명**` 이후의 서술을 뽑아냅니다.
|
||||
*
|
||||
* @param string $legacy 사람 작성 섹션 원문
|
||||
* @return string|null 보존할 서술 (없으면 null)
|
||||
*/
|
||||
private function extractLegacyProse(string $legacy): ?string
|
||||
{
|
||||
$pos = strpos($legacy, '**설명**');
|
||||
if ($pos === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$prose = trim(substr($legacy, $pos));
|
||||
|
||||
if ($prose === '' || Str::contains($prose, 'TODO: 이 엔드포인트의 용도')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $prose;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 문서에서 라우트의 `@generated` 블록 본문만 잘라냅니다 (헤딩 제외).
|
||||
*
|
||||
* 헤딩까지 함께 되살리는 {@see self::extractGeneratedBlock()} 과 달리, 표 셀 대조에만
|
||||
* 쓰이므로 블록 본문만 필요하다.
|
||||
*
|
||||
* @param string $existing 기존 문서 전체
|
||||
* @param string $key 라우트명(생성 블록 키)
|
||||
* @return string|null 블록 본문 (없으면 null)
|
||||
*/
|
||||
private function extractGeneratedBlockBody(string $existing, string $key): ?string
|
||||
{
|
||||
$startPos = strpos($existing, self::GEN_START.$key.' -->');
|
||||
if ($startPos === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$endPos = strpos($existing, self::GEN_END, $startPos);
|
||||
if ($endPos === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return substr($existing, $startPos, $endPos - $startPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 표 블록에서 `행 키 => 설명` 맵을 수집합니다 (TODO 셀은 제외).
|
||||
*
|
||||
* @param string $block 생성 블록 본문
|
||||
* @return array<string, string> 행 키 => 설명
|
||||
*/
|
||||
private function collectRowDescriptions(string $block): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
foreach (explode("\n", $block) as $line) {
|
||||
if (! str_starts_with(trim($line), '|')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cells = $this->splitRowCells($line);
|
||||
if (count($cells) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$description = trim($cells[count($cells) - 1]);
|
||||
if ($description === '' || str_contains($description, '<!-- TODO')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 구분선(`| --- | --- |`)은 건너뛴다.
|
||||
if (preg_match('/^-{2,}$/', $description)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rowKey = $this->rowKey($cells);
|
||||
if ($rowKey !== null) {
|
||||
$map[$rowKey] = $description;
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/** @var string 분해 중 인라인 코드 안 파이프를 감춰 두는 자리표시자 (문서에 등장할 수 없는 제어문자) */
|
||||
private const CELL_PIPE_PLACEHOLDER = "\x00PIPE\x00";
|
||||
|
||||
/**
|
||||
* 표 행 문자열을 셀 배열로 분해합니다 (앞뒤 파이프 제거, 셀 구분자가 아닌 파이프 보존).
|
||||
*
|
||||
* 셀 구분자가 아닌 파이프는 두 종류다. 이스케이프된 파이프(`\|`)와 **인라인 코드 스팬 안의
|
||||
* 파이프**다. 후자는 실제 문서에 흔하다 — 셸 메타문자 안내처럼 파이프 자체를 설명하는 문장이
|
||||
* 그렇다. 이를 구분자로 보면 셀 수가 늘어 마지막 셀(=설명)이 파이프 뒤 조각으로 바뀌고,
|
||||
* {@see self::collectRowDescriptions()} 가 그 조각을 사람이 쓴 설명으로 승계해 문장 앞부분이
|
||||
* 통째로 잘려 나간다. 오류 없이 문서만 훼손되므로 분해 단계에서 막는다.
|
||||
*
|
||||
* @param string $line 표 행
|
||||
* @return array<int, string> 셀 배열
|
||||
*/
|
||||
private function splitRowCells(string $line): array
|
||||
{
|
||||
$trimmed = trim($line);
|
||||
$trimmed = preg_replace('/^\|/', '', $trimmed);
|
||||
$trimmed = preg_replace('/\|$/', '', (string) $trimmed);
|
||||
|
||||
// 인라인 코드 스팬 안의 파이프를 잠시 감춘다 (분해 후 되돌린다).
|
||||
$masked = preg_replace_callback(
|
||||
'/`[^`]*`/',
|
||||
static fn (array $matches): string => str_replace('|', self::CELL_PIPE_PLACEHOLDER, $matches[0]),
|
||||
(string) $trimmed
|
||||
);
|
||||
|
||||
// 이스케이프된 파이프(`\|`)도 셀 구분자가 아니다.
|
||||
$parts = preg_split('/(?<!\\\\)\|/', (string) $masked);
|
||||
|
||||
if ($parts === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(
|
||||
static fn (string $cell): string => str_replace(self::CELL_PIPE_PLACEHOLDER, '|', $cell),
|
||||
$parts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 표 행의 식별 키를 만듭니다.
|
||||
*
|
||||
* 파라미터 표는 `이름 + 위치(query/body/path)` 로, 응답 필드/에러 표는 이름(또는 상태코드)만으로
|
||||
* 식별한다. 두 표의 키가 섞이지 않도록 두 번째 셀이 위치 값일 때만 결합한다.
|
||||
*
|
||||
* @param array<int, string> $cells 셀 배열
|
||||
* @return string|null 행 키 (식별 불가 시 null)
|
||||
*/
|
||||
private function rowKey(array $cells): ?string
|
||||
{
|
||||
$name = trim($cells[0]);
|
||||
if ($name === '' || preg_match('/^-{2,}$/', $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$second = isset($cells[1]) ? trim($cells[1]) : '';
|
||||
if (in_array($second, ['query', 'body', 'path', 'header'], true)) {
|
||||
return $name.'@'.$second;
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* README 헤더(인용 블록)와 첫 생성 블록 사이의 사람 개요를 추출합니다.
|
||||
*
|
||||
|
||||
@@ -125,11 +125,31 @@ README.md "API 레퍼런스" 또는 AGENTS.md "API 레퍼런스 진입점" 표
|
||||
등 파일시스템·프로세스·외부 네트워크 접촉)는 실측에서 제외(`side-effectful-write`)하고 요청 예시만 정적 방출한다.
|
||||
- **실측 제외**(부수효과 쓰기·바이너리·미치환 path 파라미터 등) 엔드포인트: 요청 예시는 파라미터 표 기반으로 정적
|
||||
조립(raw HTTP 요청 골격), 응답 예시는 `<!-- 실측 제외: {사유} — 응답 예시는 사람이 작성하세요. -->` 마커로 남긴다.
|
||||
- 두 예시 블록은 전량 `@generated` 블록 **내부**에 있다. 다만 이미 파라미터/응답 필드 표의 사람 서술이
|
||||
채워진 문서에 예시를 추가할 때는 전체 재생성(`api:docgen`)을 쓰지 않는다 — 전체 재생성은 표를 통째로
|
||||
다시 조립해 정적 추출로 재현 불가능한 도메인 서술 셀을 TODO 로 되돌린다. 대신 `api:docgen --examples-only`
|
||||
로 표·서술을 건드리지 않고 예시 2블록만 in-place 삽입/치환한다(측정은 재수행하되 표는 불가침). 백필
|
||||
커맨드(`backfill-params`/`backfill-fields`)도 예시 블록을 건드리지 않는다.
|
||||
- 두 예시 블록은 전량 `@generated` 블록 **내부**에 있다. 이미 파라미터/응답 필드 표의 사람 서술이
|
||||
채워진 문서에 예시만 추가할 때는 `api:docgen --examples-only` 로 표·서술을 건드리지 않고 예시 2블록만
|
||||
in-place 삽입/치환한다(측정은 재수행하되 표는 불가침). 백필 커맨드(`backfill-params`/`backfill-fields`)도
|
||||
예시 블록을 건드리지 않는다.
|
||||
|
||||
### 재생성이 지우지 못하는 것
|
||||
|
||||
전체 재생성(`api:docgen`)은 표를 통째로 다시 조립하지만, **이미 문서에 있던 사실을 지우지 않는다**.
|
||||
"이번 실행이 알아내지 못했다" 는 "그 사실이 없다" 와 다르기 때문이다. 병합 단계가 다음을 보존한다.
|
||||
|
||||
| 대상 | 규칙 |
|
||||
| --- | --- |
|
||||
| 표의 설명 셀 | 기존 설명이 있으면 유지. 도구가 만드는 문구는 필드명에서 유추한 일반 문장이라 사람이 적은 도메인 사실보다 정보량이 적다. 값이 바뀌는 컬럼(타입·예시·허용값)은 그대로 재생성된다 |
|
||||
| 엔드포인트 서술(`**설명**`) | 생성 블록 뒤의 사람 서술을 원문 그대로 되살린다. 생성 마커가 없던 수기 섹션은 헤딩(`### METHOD /uri`)으로 찾아 보존한다 |
|
||||
| 응답 필드 표 | 이번 실측이 빈 응답이라 표가 사라지는 경우 기존 표를 유지한다 |
|
||||
| 에러 응답 구간 | 새 문구가 TODO 스텁일 때만 기존 서술을 이어받는다 |
|
||||
|
||||
설명을 갱신하려면 그 셀을 직접 고치거나 비운다 — 비운 셀은 다음 재생성이 채운다.
|
||||
|
||||
### 응답 예시 크기 상한
|
||||
|
||||
응답 예시는 pretty JSON 기준 8KB 를 넘지 않는다. 목록 형태(`data.data[]`)는 대표 2건으로 자르고, 목록이
|
||||
아닌 큰 응답(편집기 스펙·컴포넌트 카탈로그·라우트 목록 등)은 절단 강도를 단계적으로 올려 상한 이하로
|
||||
줄인다. 잘린 자리에는 `... (총 N건 중 K건 표시)` / `(N개 키 생략, 총 M개)` 표기를 남겨 무엇이 빠졌는지
|
||||
문서에 드러낸다. 상한이 없으면 문서 한 파일이 수 MB 가 되어 리뷰·git·에디터가 모두 망가진다.
|
||||
- **`--examples-only` 모드**: 기존 문서의 각 엔드포인트 `@generated` 블록에 요청 예시(응답 필드 표 앞)와
|
||||
응답 예시(에러 응답 표 앞)만 삽입한다. 이미 예시가 있으면 그 블록만 새 실측으로 치환(멱등, 실측 상대시간
|
||||
필드는 재측정으로 값이 달라질 수 있음). 표·필드 설명·엔드포인트 서술은 전량 보존된다. 최초 스캐폴딩이
|
||||
|
||||
@@ -315,6 +315,82 @@ public function toListArray(): array
|
||||
|
||||
---
|
||||
|
||||
## 목록 응답의 하위 컬렉션
|
||||
|
||||
목록은 화면이 그 행에서 **실제로 그리는 것**만 싣는다. 행마다 하위 컬렉션을 통째로 직렬화하면 한 페이지를 여는 것만으로 수백~수천 행이 응답에 실린다.
|
||||
|
||||
### 가짜 가드
|
||||
|
||||
```php
|
||||
// ❌ 가드처럼 보이지만 가드가 아니다
|
||||
'options' => $this->relationLoaded('options')
|
||||
? ProductOptionResource::collection($this->options)
|
||||
: $this->whenLoaded('activeOptions'),
|
||||
```
|
||||
|
||||
Repository 가 목록 쿼리에서 `options` 를 무조건 eager load 하면 이 조건은 **항상 참**이 되어, 방어하는 것처럼 보이는 코드가 실제로는 아무것도 막지 못한다.
|
||||
|
||||
```php
|
||||
// ✅ whenLoaded 하나만 — 로드 여부는 Repository 가 결정한다
|
||||
'options' => $this->whenLoaded('options', fn () => ProductOptionResource::collection($this->options)),
|
||||
```
|
||||
|
||||
### 개수·합계는 DB 집계로
|
||||
|
||||
배열을 실어 보내고 화면에서 세지 않는다. PHP 컬렉션 연산(`$this->options->where(...)->sum(...)`)도 같은 문제다 — 세려면 이미 전부 메모리에 올라와 있어야 한다.
|
||||
|
||||
```php
|
||||
// ✅ Repository 의 withCount / withSum 결과를 그대로 노출
|
||||
'options_count' => $this->whenHas('options_count', fn () => (int) $this->options_count),
|
||||
```
|
||||
|
||||
집계 별칭의 존재 여부는 값 검사(`!== null`)로 판정할 수 없다. `SUM` 은 대상 0건에서 `NULL` 을 돌려주므로 "집계하지 않았다" 와 "집계했더니 비어 있다" 가 구분되지 않는다. 속성 키의 존재 여부(`array_key_exists`)만이 두 상황을 가른다.
|
||||
|
||||
### 대표 1건이 필요할 때
|
||||
|
||||
목록이 하위 컬렉션의 **첫 1건만** 그린다면 관계 자체를 1건으로 좁힌다. eager load 의 `limit(1)` 은 부모별이 아니라 배치 쿼리 전체에 걸리므로 첫 행만 값을 받고 나머지는 빈 값이 된다.
|
||||
|
||||
```php
|
||||
// 모델 — 관계를 "가장 오래된 1건" 으로 정의
|
||||
public function firstOption(): HasOne
|
||||
{
|
||||
return $this->hasOne(OrderOption::class, 'order_id')->oldestOfMany();
|
||||
}
|
||||
```
|
||||
|
||||
### 목록 표현은 명시 호출
|
||||
|
||||
`toListArray()` 를 정의해 두고 컬렉션이 `toArray()` 를 호출하면 경량 표현은 쓰이지 않는다. 컨트롤러나 컬렉션이 목록 표현을 **명시적으로** 부르는지 확인한다.
|
||||
|
||||
```php
|
||||
// ✅ 컬렉션이 목록 표현을 명시 호출
|
||||
'data' => $this->collection->map(fn ($row) => (new SampleResource($row))->toListArray($request))->all(),
|
||||
```
|
||||
|
||||
이 형태는 Laravel 의 `MissingValue` 제거 단계를 거치지 않으므로, 조건부 필드를 직접 걸러내야 한다(위 "커스텀 메서드" 절 참조). 걸러내지 않으면 미충족 필드가 `{}` 로 응답에 남는다.
|
||||
|
||||
### 목록 Resource 안에서 관계를 재쿼리하지 않는다
|
||||
|
||||
```php
|
||||
// ❌ eager load 된 컬렉션을 무시하고 행마다 쿼리
|
||||
'thumbnail' => $this->images()->first()?->url,
|
||||
|
||||
// ✅ 로드된 컬렉션에서 고른다
|
||||
'thumbnail' => $this->relationLoaded('images')
|
||||
? $this->images->firstWhere('is_thumbnail', true)?->url
|
||||
: null,
|
||||
```
|
||||
|
||||
### 뺀 값에는 대체 경로가 있어야 한다
|
||||
|
||||
목록에서 제거한 값은 (a) 집계로 대체되거나 (b) 단건 조회에 그대로 남아 있어야 한다. 어느 쪽도 아니면 그것은 성능 개선이 아니라 기능 삭제다.
|
||||
|
||||
착수 전 **소비처를 실측한다.** 화면이 그 값을 실제로 순회·렌더하면 제거는 기능 축소다 — 계획에 "안 쓴다" 고 적혀 있어도 레이아웃 JSON 을 열어 확인한다.
|
||||
|
||||
단건 조회가 대체 경로라면, 단건이 목록용 조회를 **재사용하지 않는지** 확인한다. 목록 조회는 컬럼을 좁히고 건수 상한을 두므로, 단건이 그것을 재사용하면 목록 최적화가 그대로 단건의 기능 삭제가 된다.
|
||||
|
||||
---
|
||||
|
||||
## 패턴 예시
|
||||
|
||||
### 기본 리소스 클래스
|
||||
|
||||
@@ -154,7 +154,7 @@ location ~* \.(js|css|json)$ { expires max; access_log off; }
|
||||
## 코어 API 레퍼런스
|
||||
|
||||
<!-- @generated:start:api-readme-index -->
|
||||
- **문서 수**: 35 · **엔드포인트 수**: 291
|
||||
- **문서 수**: 36 · **엔드포인트 수**: 319
|
||||
|
||||
| 문서 | 도메인 | 엔드포인트 |
|
||||
| --- | --- | --- |
|
||||
@@ -190,6 +190,7 @@ location ~* \.(js|css|json)$ { expires max; access_log off; }
|
||||
| [search.md](search.md) | `search` | 1 |
|
||||
| [seo.md](seo.md) | `seo` | 5 |
|
||||
| [settings.md](settings.md) | `settings` | 15 |
|
||||
| [system.md](system.md) | `system` | 2 |
|
||||
| [templates.md](templates.md) | `templates` | 57 |
|
||||
| [users.md](users.md) | `users` | 12 |
|
||||
| [verify-password.md](verify-password.md) | `verify-password` | 1 |
|
||||
@@ -201,13 +202,13 @@ location ~* \.(js|css|json)$ { expires max; access_log off; }
|
||||
> 각 확장이 자신의 API 문서를 소유합니다. 아래 표는 자동 생성됩니다.
|
||||
|
||||
<!-- @generated:start:api-readme-extensions -->
|
||||
- **확장 수**: 13 · **엔드포인트 수**: 390
|
||||
- **확장 수**: 13 · **엔드포인트 수**: 391
|
||||
|
||||
| 확장 | 유형 | API 문서 목차 | 문서/엔드포인트 |
|
||||
| --- | --- | --- | --- |
|
||||
| `gnuboard7-hello_module` | 모듈 | [docs/api/](../../../modules/_bundled/gnuboard7-hello_module/docs/api/README.md) | 1 / 7 |
|
||||
| `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-ecommerce` | 모듈 | [docs/api/](../../../modules/_bundled/sirsoft-ecommerce/docs/api/README.md) | 33 / 232 |
|
||||
| `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 |
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Activity Logs 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -57,25 +57,25 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| number | integer | `87675` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) |
|
||||
| id | integer | `210842` | 기본 키 (내부 식별자) |
|
||||
| number | integer | `88816` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) |
|
||||
| id | integer | `89118` | 기본 키 (내부 식별자) |
|
||||
| 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) |
|
||||
| loggable_type | string | `App\Models\User` | 로그가 연관된 대상 리소스의 모델 클래스 FQCN (loggable 다형성 관계 타입) |
|
||||
| loggable_type_display | string | `User` | `loggable_type` 의 표시용 짧은 이름 (네임스페이스 제외 클래스명 파생) |
|
||||
| loggable_id | integer | `1209` | loggable 식별자 (연관 리소스 참조) |
|
||||
| action | string | `auth.login` | 액션 유형 (created, updated, deleted, login, export 등) |
|
||||
| action_label | string | `로그인` | `action` 값의 사람이 읽는 라벨 (현지화/Enum 파생) |
|
||||
| localized_description | string | `관리자 로그인` | `description` 의 현재 로케일 해석 값 (다국어 필드를 표시용 문자열로 해석) |
|
||||
| description_key | string | `activity_log.description.auth_login` | 다국어 번역 키 (예: activity_log.description.user_create) |
|
||||
| properties | object | `{"result_count":82}` | 변경 상세 데이터 (old/new 값) |
|
||||
| changes | array | `[{"field":"stock_quantity","label_key":"sirsoft-ecommerce…` | 구조화된 변경 이력 (필드별 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 에 "시스템" 라벨만 담김 |
|
||||
| actor_name | string | `최고관리자` | 행위를 수행한 주체(사용자/시스템)의 이름 |
|
||||
| user | object | `{"uuid":"a26219fc-94a0-4f63-9404-04c2a6ac99e4","name":"최고…` | 행위를 수행한 사용자 정보 (uuid/name/email). 시스템 발생 로그로 사용자가 없으면 name 에 "시스템" 라벨만 담김 |
|
||||
| ip_address | string | `127.0.0.1` | IP 주소 (IPv6 대응) |
|
||||
| created_at | string | `2026-07-07 10:00:47` | 생성 일시 |
|
||||
| created_at | string | `2026-08-04 19:00:10` | 생성 일시 |
|
||||
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
|
||||
| abilities | object | `{"can_read":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
@@ -92,31 +92,29 @@ HTTP/1.1 200
|
||||
"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
|
||||
},
|
||||
"number": 88816,
|
||||
"id": 89118,
|
||||
"log_type": "user",
|
||||
"log_type_label": "사용자",
|
||||
"loggable_type": "App\\Models\\User",
|
||||
"loggable_type_display": "User",
|
||||
"loggable_id": 1209,
|
||||
"action": "auth.login",
|
||||
"action_label": "로그인",
|
||||
"localized_description": "관리자 로그인",
|
||||
"description_key": "activity_log.description.auth_login",
|
||||
"properties": null,
|
||||
"changes": null,
|
||||
"bulk_changes": null,
|
||||
"has_changes": false,
|
||||
"actor_name": "API 문서 샘플 사용자",
|
||||
"actor_name": "최고관리자",
|
||||
"user": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 사용자",
|
||||
"email": "apidoc-sample-user@example.com"
|
||||
"uuid": "a26219fc-94a0-4f63-9404-04c2a6ac99e4",
|
||||
"name": "최고관리자",
|
||||
"email": "heuristing@gmail.com"
|
||||
},
|
||||
"ip_address": "127.0.0.1",
|
||||
"created_at": "2026-07-08 12:12:12",
|
||||
"created_at": "2026-08-04 19:00:10",
|
||||
"is_owner": true,
|
||||
"abilities": {
|
||||
"can_read": true,
|
||||
@@ -124,47 +122,45 @@ HTTP/1.1 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"number": 21,
|
||||
"id": 21,
|
||||
"number": 88815,
|
||||
"id": 89117,
|
||||
"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
|
||||
},
|
||||
"loggable_type": "App\\Models\\TemplateLayout",
|
||||
"loggable_type_display": "TemplateLayout",
|
||||
"loggable_id": 62,
|
||||
"action": "layout.update",
|
||||
"action_label": "수정",
|
||||
"localized_description": "레이아웃 수정 (home)",
|
||||
"description_key": "activity_log.description.layout_update",
|
||||
"properties": null,
|
||||
"changes": null,
|
||||
"bulk_changes": null,
|
||||
"has_changes": false,
|
||||
"actor_name": "API 문서 샘플 사용자",
|
||||
"actor_name": "최고관리자",
|
||||
"user": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 사용자",
|
||||
"email": "apidoc-sample-user@example.com"
|
||||
"uuid": "a26219fc-94a0-4f63-9404-04c2a6ac99e4",
|
||||
"name": "최고관리자",
|
||||
"email": "heuristing@gmail.com"
|
||||
},
|
||||
"ip_address": "127.0.0.1",
|
||||
"created_at": "2026-07-08 12:08:26",
|
||||
"created_at": "2026-08-04 17:40:37",
|
||||
"is_owner": true,
|
||||
"abilities": {
|
||||
"can_read": true,
|
||||
"can_delete": true
|
||||
}
|
||||
},
|
||||
"... (총 22건 중 2건 표시)"
|
||||
"... (총 25건 중 2건 표시)"
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"last_page": 3553,
|
||||
"per_page": 25,
|
||||
"total": 22,
|
||||
"total": 88816,
|
||||
"from": 1,
|
||||
"to": 22,
|
||||
"has_more_pages": false
|
||||
"to": 25,
|
||||
"has_more_pages": true
|
||||
},
|
||||
"abilities": {
|
||||
"can_delete": true
|
||||
@@ -216,7 +212,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -250,7 +246,7 @@ Content-Type: application/json
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/admin/activity-logs/1 HTTP/1.1
|
||||
DELETE /api/admin/activity-logs/159 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Attachment 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Attachments 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -34,9 +34,7 @@
|
||||
| source_type | body | string | 아니오 | — | 첨부 생성 출처 구분 (AttachmentSourceType Enum — core: 코어 시스템, module: 모듈, plugin: 플러그인). 미지정 시 core로 기본 설정 |
|
||||
| source_identifier | body | string | 아니오 | max 255 | 출처 식별자 |
|
||||
|
||||
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.attachment.upload_validation_rules`).
|
||||
>
|
||||
> 업로드 가능한 확장자는 환경설정의 허용 형식 목록을 따르며, 확장은 `core.attachment.allowed_extensions` 필터로 목록을 조정할 수 있습니다 (허용 목록이 비어 있으면 확장자를 제한하지 않습니다).
|
||||
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.attachment.upload_validation_rules`, `core.attachment.allowed_extensions`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
@@ -77,7 +75,7 @@ example-key
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -113,9 +111,7 @@ example-key
|
||||
| source_type | body | string | 아니오 | — | 첨부 생성 출처 구분 (AttachmentSourceType Enum — core: 코어 시스템, module: 모듈, plugin: 플러그인). 미지정 시 core로 기본 설정 |
|
||||
| source_identifier | body | string | 아니오 | max 255 | 출처 식별자 |
|
||||
|
||||
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.attachment.upload_batch_validation_rules`).
|
||||
>
|
||||
> 업로드 가능한 확장자는 환경설정의 허용 형식 목록을 따르며, 확장은 `core.attachment.allowed_extensions` 필터로 목록을 조정할 수 있습니다 (허용 목록이 비어 있으면 확장자를 제한하지 않습니다).
|
||||
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.attachment.upload_batch_validation_rules`, `core.attachment.allowed_extensions`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
@@ -140,7 +136,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -191,7 +187,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -233,7 +229,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
+106
-2176
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Avatar 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -38,7 +38,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -67,8 +67,6 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| avatar | body | image | 예 | max 2048 | 아바타 이미지 |
|
||||
|
||||
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.user.upload_avatar_validation_rules`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
@@ -88,7 +86,7 @@ Content-Type: image/png
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Broadcasting 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -37,7 +37,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
+5
-1529
File diff suppressed because it is too large
Load Diff
+105
-65
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Dashboard 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -41,12 +41,12 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| type | string | `user` | 활동 분류 (log_type Enum 값 — admin: 관리자, user: 사용자, system: 시스템) |
|
||||
| icon | string | `circle-info` | 아이콘 식별자 (아이콘 클래스/이름) |
|
||||
| icon | string | `right-to-bracket` | 아이콘 식별자 (아이콘 클래스/이름) |
|
||||
| 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) |
|
||||
| title | string | `관리자 로그인` | 제목 |
|
||||
| description | string | `최고관리자` | 설명 (다국어 필드는 로케일별 값 객체) |
|
||||
| time | string | `2시간 전` | 상대 시각 표시 (예: "24초 전" — diffForHumans() 산물) |
|
||||
| timestamp | string | `2026-08-04T19:00:10+09:00` | 활동 발생 절대 시각 (created_at 을 사용자 타임존으로 변환한 ISO 8601) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -59,50 +59,50 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "최근 활동을 성공적으로 조회했습니다.",
|
||||
"data": [
|
||||
{
|
||||
"type": "user",
|
||||
"icon": "right-to-bracket",
|
||||
"icon_color": "green",
|
||||
"title": "관리자 로그인",
|
||||
"description": "최고관리자",
|
||||
"time": "2시간 전",
|
||||
"timestamp": "2026-08-04T19:00:10+09:00"
|
||||
},
|
||||
{
|
||||
"type": "admin",
|
||||
"icon": "circle-info",
|
||||
"icon": "pen-to-square",
|
||||
"icon_color": "blue",
|
||||
"title": "사용자 목록 조회",
|
||||
"description": "API 문서 샘플 사용자",
|
||||
"time": "2분 전",
|
||||
"timestamp": "2026-07-08T12:12:12+09:00"
|
||||
"title": "레이아웃 수정 (home)",
|
||||
"description": "최고관리자",
|
||||
"time": "4시간 전",
|
||||
"timestamp": "2026-08-04T17:40:37+09:00"
|
||||
},
|
||||
{
|
||||
"type": "admin",
|
||||
"icon": "circle-info",
|
||||
"icon_color": "blue",
|
||||
"title": "사용자 목록 조회",
|
||||
"description": "API 문서 샘플 사용자",
|
||||
"time": "6분 전",
|
||||
"timestamp": "2026-07-08T12:08:26+09:00"
|
||||
"description": "최고관리자",
|
||||
"time": "4시간 전",
|
||||
"timestamp": "2026-08-04T17:37:43+09:00"
|
||||
},
|
||||
{
|
||||
"type": "admin",
|
||||
"icon": "circle-info",
|
||||
"icon_color": "blue",
|
||||
"title": "사용자 목록 조회",
|
||||
"description": "API 문서 샘플 사용자",
|
||||
"time": "9분 전",
|
||||
"timestamp": "2026-07-08T12:04:38+09:00"
|
||||
"type": "user",
|
||||
"icon": "right-to-bracket",
|
||||
"icon_color": "green",
|
||||
"title": "관리자 로그인",
|
||||
"description": "최고관리자",
|
||||
"time": "4시간 전",
|
||||
"timestamp": "2026-08-04T17:35:37+09:00"
|
||||
},
|
||||
{
|
||||
"type": "admin",
|
||||
"type": "user",
|
||||
"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"
|
||||
"icon_color": "green",
|
||||
"title": "주문 옵션 부분 취소 (옵션 ID: 1440)",
|
||||
"description": "최고관리자",
|
||||
"time": "4시간 전",
|
||||
"timestamp": "2026-08-04T17:23:44+09:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -194,14 +194,14 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `677` | 기본 키 (내부 식별자) |
|
||||
| id | integer | `7559` | 기본 키 (내부 식별자) |
|
||||
| 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) |
|
||||
| time | string | `3일 전` | 상대 시각 표시 (예: "24초 전" — diffForHumans() 산물) |
|
||||
| timestamp | string | `2026-07-31T21:55:00+09:00` | 발송 절대 시각 (sent_at, 없으면 created_at 을 사용자 타임존으로 변환한 ISO 8601) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -215,14 +215,54 @@ HTTP/1.1 200
|
||||
"message": "최근 알림을 성공적으로 조회했습니다.",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"id": 7559,
|
||||
"type": "apidoc.sample.event",
|
||||
"channel": "mail",
|
||||
"recipient": "API 문서 샘플 사용자",
|
||||
"subject": "API 문서 샘플 알림",
|
||||
"status": "sent",
|
||||
"time": "2시간 전",
|
||||
"timestamp": "2026-07-08T09:41:24+09:00"
|
||||
"time": "3일 전",
|
||||
"timestamp": "2026-07-31T21:55:00+09:00"
|
||||
},
|
||||
{
|
||||
"id": 7558,
|
||||
"type": "inquiry_received",
|
||||
"channel": "mail",
|
||||
"recipient": "송민성",
|
||||
"subject": "",
|
||||
"status": "skipped",
|
||||
"time": "4일 전",
|
||||
"timestamp": "2026-07-31T21:41:59+09:00"
|
||||
},
|
||||
{
|
||||
"id": 7557,
|
||||
"type": "inquiry_received",
|
||||
"channel": "mail",
|
||||
"recipient": "석지은",
|
||||
"subject": "",
|
||||
"status": "skipped",
|
||||
"time": "4일 전",
|
||||
"timestamp": "2026-07-31T21:41:59+09:00"
|
||||
},
|
||||
{
|
||||
"id": 7556,
|
||||
"type": "inquiry_received",
|
||||
"channel": "mail",
|
||||
"recipient": "심상수",
|
||||
"subject": "",
|
||||
"status": "skipped",
|
||||
"time": "4일 전",
|
||||
"timestamp": "2026-07-31T21:41:59+09:00"
|
||||
},
|
||||
{
|
||||
"id": 7555,
|
||||
"type": "inquiry_received",
|
||||
"channel": "mail",
|
||||
"recipient": "임아름",
|
||||
"subject": "",
|
||||
"status": "skipped",
|
||||
"time": "4일 전",
|
||||
"timestamp": "2026-07-31T21:41:59+09:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -265,9 +305,9 @@ _단건 응답: `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 폴백 |
|
||||
| cpu | object | `{"percentage":21,"color":"green"}` | CPU 사용률 정보 (percentage: 0~100 사용률, color: 임계 색상 — green<50, blue 50~69, yellow 70~89, red≥90) |
|
||||
| memory | object | `{"percentage":77,"used":"24.3 GB","total":"31.5 GB","colo…` | 메모리 사용량 정보 (percentage 사용률, used/total: 사용량·총량 형식화 문자열, color: 임계 색상). 수집 불가 시 percentage 0·"알 수 없음"·color gray 폴백 |
|
||||
| disk | object | `{"percentage":86,"used":"408.2 GB","total":"474.7 GB","co…` | 디스크 사용량 정보 (percentage 사용률, used/total: 사용량·총량 형식화 문자열, color: 임계 색상). 수집 불가 시 percentage 0·"알 수 없음"·color gray 폴백 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -281,18 +321,18 @@ HTTP/1.1 200
|
||||
"message": "시스템 리소스 정보를 성공적으로 조회했습니다.",
|
||||
"data": {
|
||||
"cpu": {
|
||||
"percentage": 52,
|
||||
"color": "blue"
|
||||
"percentage": 21,
|
||||
"color": "green"
|
||||
},
|
||||
"memory": {
|
||||
"percentage": 86,
|
||||
"used": "27 GB",
|
||||
"percentage": 77,
|
||||
"used": "24.3 GB",
|
||||
"total": "31.5 GB",
|
||||
"color": "yellow"
|
||||
},
|
||||
"disk": {
|
||||
"percentage": 76,
|
||||
"used": "362.6 GB",
|
||||
"percentage": 86,
|
||||
"used": "408.2 GB",
|
||||
"total": "474.7 GB",
|
||||
"color": "yellow"
|
||||
}
|
||||
@@ -337,11 +377,11 @@ _단건 응답: `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) |
|
||||
| total_users | object | `{"count":82,"change_percent":0,"change_display":"+82","tr…` | 전체 사용자 수 (통계 객체는 count/추이 포함) |
|
||||
| installed_modules | object | `{"total":4,"active":4}` | 설치된 모듈 집계 객체 (total/active) |
|
||||
| active_plugins | object | `{"total":8,"active":8}` | 활성 플러그인 집계 객체 (total/active) |
|
||||
| installed_templates | object | `{"total":2,"active":2}` | 설치된 템플릿 집계 객체 (total/active) |
|
||||
| language_packs | object | `{"total":20,"active":16}` | 언어팩 집계 객체 (active: 현재 활성 언어팩 수, total: 활성 + 미설치 번들 팩 수) |
|
||||
| language_packs | object | `{"total":22,"active":2}` | 언어팩 집계 객체 (active: 현재 활성 언어팩 수, total: 활성 + 미설치 번들 팩 수) |
|
||||
| system_status | object | `{"status":"normal","label":"정상","all_services_running":true}` | 시스템 상태 객체 (status: normal 정상 / warning 경고, label: 상태 다국어 라벨, all_services_running: 전체 서비스 정상 동작 여부) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -356,26 +396,26 @@ HTTP/1.1 200
|
||||
"message": "대시보드 통계를 성공적으로 조회했습니다.",
|
||||
"data": {
|
||||
"total_users": {
|
||||
"count": 2,
|
||||
"count": 82,
|
||||
"change_percent": 0,
|
||||
"change_display": "+2",
|
||||
"change_display": "+82",
|
||||
"trend": "up"
|
||||
},
|
||||
"installed_modules": {
|
||||
"total": 3,
|
||||
"active": 3
|
||||
"total": 4,
|
||||
"active": 4
|
||||
},
|
||||
"active_plugins": {
|
||||
"total": 9,
|
||||
"active": 0
|
||||
"total": 8,
|
||||
"active": 8
|
||||
},
|
||||
"installed_templates": {
|
||||
"total": 2,
|
||||
"active": 0
|
||||
"active": 2
|
||||
},
|
||||
"language_packs": {
|
||||
"total": 17,
|
||||
"active": 1
|
||||
"total": 22,
|
||||
"active": 2
|
||||
},
|
||||
"system_status": {
|
||||
"status": "normal",
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Extensions 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -43,7 +43,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| items | object | `{"plugins":[],"modules":[],"templates":[]}` | 코어 비호환으로 자동 비활성화된 확장을 타입별(`plugins`/`modules`/`templates`)로 묶은 목록. 각 원소는 식별자(`identifier`), 비호환 요구 버전(`incompatible_required_version`), 비활성화 시각(`deactivated_at`)을 가지며, 사용자가 dismiss했거나 hidden(학습용 샘플) 확장은 제외됨 |
|
||||
| current_core_version | string | `7.0.1` | 현재 설치된 코어 버전 |
|
||||
| current_core_version | string | `7.0.6` | 현재 설치된 코어 버전 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -61,7 +61,7 @@ HTTP/1.1 200
|
||||
"modules": [],
|
||||
"templates": []
|
||||
},
|
||||
"current_core_version": "7.0.1"
|
||||
"current_core_version": "7.0.6"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -88,7 +88,7 @@ HTTP/1.1 200
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| type | path | string | 예 | module, plugin, template | 대상 확장의 타입 (module: 모듈, plugin: 플러그인, template: 템플릿). 타입에 맞는 Repository/Manager를 해석하는 데 사용되며 그 외 값은 422 |
|
||||
| type | path | string | 예 | — | 대상 확장의 타입 (module: 모듈, plugin: 플러그인, template: 템플릿). 타입에 맞는 Repository/Manager를 해석하는 데 사용되며 그 외 값은 422 |
|
||||
| identifier | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
@@ -102,7 +102,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -131,7 +131,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| type | path | string | 예 | module, plugin, template | 대상 확장의 타입 (module: 모듈, plugin: 플러그인, template: 템플릿). 타입에 맞는 Repository/Manager를 해석하는 데 사용되며 그 외 값은 422 |
|
||||
| type | path | string | 예 | — | 대상 확장의 타입 (module: 모듈, plugin: 플러그인, template: 템플릿). 타입에 맞는 Repository/Manager를 해석하는 데 사용되며 그 외 값은 422 |
|
||||
| identifier | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
@@ -145,7 +145,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
+295
-205
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Identity 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -64,25 +64,25 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | string | `e6ab6cd6-cdff-46cd-b4c2-b89dfda8745b` | 기본 키 (내부 식별자) |
|
||||
| id | string | `06f077b4-fc53-4dad-81ff-846cd6426a0f` | 기본 키 (내부 식별자) |
|
||||
| provider_id | string | `inicis` | provider 식별자 (연관 리소스 참조) |
|
||||
| purpose | string | `sensitive_action` | 인증 목적 (signup/password_reset/self_update/sensitive_action/login 또는 모듈 정의 목적) |
|
||||
| channel | string | `ipin` | 인증에 사용된 전송 채널 (email 등 코어 채널 또는 모듈 provider 자체 식별자) |
|
||||
| user_id | integer | `130` | user 식별자 (연관 리소스 참조) |
|
||||
| target_hash | string | `d88d36166bbeffc41eb6994e390f4715280c1…` | 인증 대상 해시 (SHA256(email\|phone) — PII 원본 저장 회피) |
|
||||
| status | string | `cancelled` | 인증 시도 결과 상태 (requested/sent/processing/verified/failed/expired/cancelled/policy_violation_logged) |
|
||||
| attempts | integer | `0` | 현재까지 누적된 검증 시도 횟수 |
|
||||
| max_attempts | integer | `0` | 허용되는 최대 검증 시도 횟수 (초과 시 실패 처리) |
|
||||
| ip_address | string | `127.0.0.1` | 요청/행위가 발생한 IP 주소 |
|
||||
| purpose | string | `checkout_verification` | 인증 목적 (signup/password_reset/self_update/sensitive_action/login 또는 모듈 정의 목적) |
|
||||
| channel | string | `email` | 인증에 사용된 전송 채널 (email 등 코어 채널 또는 모듈 provider 자체 식별자) |
|
||||
| user_id | integer | `1209` | user 식별자 (연관 리소스 참조) |
|
||||
| target_hash | string | `eebbbc80cbc838162aaa9437c7aba415dc81d…` | 인증 대상 해시 (SHA256(email\|phone) — PII 원본 저장 회피) |
|
||||
| status | string | `verified` | 인증 시도 결과 상태 (requested/sent/processing/verified/failed/expired/cancelled/policy_violation_logged) |
|
||||
| attempts | integer | `2` | 현재까지 누적된 검증 시도 횟수 |
|
||||
| max_attempts | integer | `5` | 허용되는 최대 검증 시도 횟수 (초과 시 실패 처리) |
|
||||
| ip_address | string | `59.16.7.205` | 요청/행위가 발생한 IP 주소 |
|
||||
| user_agent | string | `Mozilla/5.0 (Windows NT 10.0; Win64; …` | 요청 클라이언트의 User-Agent 문자열 |
|
||||
| origin_type | string | `api` | 인증 트리거 출처 유형 (route/hook/policy/middleware/api/custom/system — IdentityOriginType) |
|
||||
| origin_identifier | string | `/api/identity/challenges` | 실제 트리거 경로/훅명 (예: PUT /api/me/password, core.user.before_update) |
|
||||
| origin_policy_key | null | `null` | 정책이 인증을 강제한 경우 해당 identity_policies.key (정책 외 트리거는 null) |
|
||||
| properties | null | `null` | 요청 페이로드 요약 (감사용 부가 정보, 없으면 null) |
|
||||
| metadata | object | `{"mid":"INIiasTest","reqSvcCd":"03","mtxid_hash":"981546e…` | 프로바이더 내부 데이터 (코드 해시·외부 인증 식별자 등, PII 원본 미포함) |
|
||||
| created_at | string | `2026-06-27 19:06:17` | 생성 일시 |
|
||||
| verified_at | string | `2026-06-27 18:58:30` | verified 일시 |
|
||||
| expires_at | string | `2026-06-27 19:21:17` | expires 일시 |
|
||||
| origin_type | string | `policy` | 인증 트리거 출처 유형 (route/hook/policy/middleware/api/custom/system — IdentityOriginType) |
|
||||
| origin_identifier | string | `sirsoft-ecommerce` | 실제 트리거 경로/훅명 (예: PUT /api/me/password, core.user.before_update) |
|
||||
| origin_policy_key | string | `sirsoft-ecommerce.checkout.before_pay` | 정책이 인증을 강제한 경우 해당 identity_policies.key (정책 외 트리거는 null) |
|
||||
| properties | object | `{"code_length":6}` | 요청 페이로드 요약 (감사용 부가 정보, 없으면 null) |
|
||||
| metadata | object | `{"hint_used":"text_code"}` | 프로바이더 내부 데이터 (코드 해시·외부 인증 식별자 등, PII 원본 미포함) |
|
||||
| created_at | string | `2026-07-30 21:14:43` | 생성 일시 |
|
||||
| verified_at | string | `2026-07-30 21:21:16` | verified 일시 |
|
||||
| expires_at | string | `2026-07-30 21:29:43` | expires 일시 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -95,15 +95,67 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "messages.success",
|
||||
"data": {
|
||||
"data": [],
|
||||
"data": [
|
||||
{
|
||||
"id": "06f077b4-fc53-4dad-81ff-846cd6426a0f",
|
||||
"provider_id": "inicis",
|
||||
"purpose": "checkout_verification",
|
||||
"channel": "email",
|
||||
"user_id": null,
|
||||
"target_hash": "eebbbc80cbc838162aaa9437c7aba415dc81d33a7c2c8ad21ef11e102d8ac16b",
|
||||
"status": "verified",
|
||||
"attempts": 2,
|
||||
"max_attempts": 5,
|
||||
"ip_address": "59.16.7.205",
|
||||
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
|
||||
"origin_type": "policy",
|
||||
"origin_identifier": "sirsoft-ecommerce",
|
||||
"origin_policy_key": "sirsoft-ecommerce.checkout.before_pay",
|
||||
"properties": {
|
||||
"code_length": 6
|
||||
},
|
||||
"metadata": {
|
||||
"hint_used": "text_code"
|
||||
},
|
||||
"created_at": "2026-07-30 21:14:43",
|
||||
"verified_at": "2026-07-30 21:21:16",
|
||||
"expires_at": "2026-07-30 21:29:43"
|
||||
},
|
||||
{
|
||||
"id": "2cd9f0a8-44da-447b-96b9-81fe2174016f",
|
||||
"provider_id": "g7:core.mail",
|
||||
"purpose": "checkout_verification",
|
||||
"channel": "email",
|
||||
"user_id": null,
|
||||
"target_hash": "08f2668d172fd18e4103d0e13de6159b3f7112f4c427918104af923c6c086790",
|
||||
"status": "verified",
|
||||
"attempts": 3,
|
||||
"max_attempts": 5,
|
||||
"ip_address": "180.182.50.7",
|
||||
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
|
||||
"origin_type": "policy",
|
||||
"origin_identifier": "sirsoft-ecommerce",
|
||||
"origin_policy_key": "sirsoft-ecommerce.checkout.before_pay",
|
||||
"properties": {
|
||||
"code_length": 6
|
||||
},
|
||||
"metadata": {
|
||||
"hint_used": "text_code"
|
||||
},
|
||||
"created_at": "2026-07-30 16:58:29",
|
||||
"verified_at": "2026-07-30 16:59:06",
|
||||
"expires_at": "2026-07-30 17:13:29"
|
||||
},
|
||||
"... (총 25건 중 2건 표시)"
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"last_page": 8,
|
||||
"per_page": 25,
|
||||
"total": 0,
|
||||
"from": null,
|
||||
"to": null,
|
||||
"has_more_pages": false
|
||||
"total": 200,
|
||||
"from": 1,
|
||||
"to": 25,
|
||||
"has_more_pages": true
|
||||
},
|
||||
"abilities": {
|
||||
"can_purge": true
|
||||
@@ -157,7 +209,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| purged_count | integer | `0` | purged 개수 (집계) |
|
||||
| purged_count | integer | `200` | purged 개수 (집계) |
|
||||
| older_than_days | integer | `1` | 기준 경과 일수 (이 일수보다 오래된 대상 필터/집계) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -171,7 +223,7 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "messages.success",
|
||||
"data": {
|
||||
"purged_count": 0,
|
||||
"purged_count": 200,
|
||||
"older_than_days": 1
|
||||
}
|
||||
}
|
||||
@@ -232,20 +284,21 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
| number | integer | `1` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| provider_id | string | `g7:core.mail` | provider 식별자 (연관 리소스 참조) |
|
||||
| scope_type | string | `provider_default` | 메시지 정의 스코프 (provider_default: 프로바이더 기본 / purpose: 목적별 / policy: 정책별 — IdentityMessageScopeType) |
|
||||
| scope_value | string | `` | 스코프 값 (provider_default 빈 문자열 / purpose 목적 키 / policy 정책 키) |
|
||||
| name | object | `{"ko":"메일 본인 확인 (기본)","en":"Mail Verification (default)",…` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| description | object | `{"ko":"특정 목적이 매칭되지 않을 때 사용되는 기본 메일 템플릿","en":"Fallback ma…` | 설명 (다국어 필드는 로케일별 값 객체) |
|
||||
| scope_type | string | `purpose` | 메시지 정의 스코프 (provider_default: 프로바이더 기본 / purpose: 목적별 / policy: 정책별 — IdentityMessageScopeType) |
|
||||
| scope_value | string | `checkout_verification` | 스코프 값 (provider_default 빈 문자열 / purpose 목적 키 / policy 정책 키) |
|
||||
| name | object | `{"ko":"결제 시 본인 확인","en":"Checkout Verification"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| description | object | `{"ko":"결제 진행 전 본인\/성인 확인 인증 코드 메일","en":"Identity\/adult …` | 설명 (다국어 필드는 로케일별 값 객체) |
|
||||
| channels | array | `["mail"]` | 이 정의가 지원하는 활성 채널 목록 (현재 mail, 향후 sms 등 확장) |
|
||||
| variables | array | `[{"key":"code","description":"인증 코드 (text_code 흐름)"},{"ke…` | 템플릿에서 치환 가능한 변수 메타데이터 목록 (원소 key/description) |
|
||||
| extension_type | string | `core` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) |
|
||||
| extension_identifier | string | `core` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| extension_type | string | `module` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) |
|
||||
| extension_identifier | string | `sirsoft-ecommerce` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| is_active | boolean | `true` | active 여부 |
|
||||
| is_default | boolean | `true` | default 여부 |
|
||||
| user_overrides | array | `["name.ja"]` | 운영자가 시드 기본값에서 수정한 필드 경로 목록 (예: name.ja — 시더 재실행 시 보존 대상) |
|
||||
| templates | array | `[{"id":1,"definition_id":1,"channel":"mail","subject":{"k…` | 이 정의에 속한 채널별 하위 메시지 템플릿 목록 (원소 id/channel/subject/body 등, eager load 시에만 포함) |
|
||||
| created_at | string | `2026-05-27 15:20:18` | 생성 일시 |
|
||||
| updated_at | string | `2026-06-30 13:33:16` | 최종 수정 일시 |
|
||||
| templates | array | `[{"id":1,"definition_id":1,"channel":"mail","subject":{"k…` | 이 정의에 속한 채널별 하위 메시지 템플릿 목록 (원소 id/channel/subject/body 등). **목록 조회에서는 대표 1건만 실린다** — 전체 채널이 필요하면 단건 조회를 사용한다 |
|
||||
| templates_count | integer | `1` | templates 개수 (집계) |
|
||||
| created_at | string | `2026-07-30 18:47:09` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-30 18:47:09` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -266,89 +319,25 @@ HTTP/1.1 200
|
||||
"provider_id": "g7:core.mail",
|
||||
"scope_type": "purpose",
|
||||
"scope_value": "checkout_verification",
|
||||
"name": {
|
||||
"ko": "결제 시 본인 확인",
|
||||
"en": "Checkout Verification"
|
||||
},
|
||||
"description": {
|
||||
"ko": "결제 진행 전 본인/성인 확인 인증 코드 메일",
|
||||
"en": "Identity/adult verification code mail before checkout"
|
||||
},
|
||||
"channels": [
|
||||
"mail"
|
||||
],
|
||||
"variables": [
|
||||
{
|
||||
"key": "code",
|
||||
"description": "인증 코드 (text_code 흐름)"
|
||||
},
|
||||
{
|
||||
"key": "expire_minutes",
|
||||
"description": "만료까지 남은 분"
|
||||
},
|
||||
{
|
||||
"key": "purpose_label",
|
||||
"description": "인증 목적 라벨"
|
||||
},
|
||||
{
|
||||
"key": "app_name",
|
||||
"description": "사이트명"
|
||||
},
|
||||
{
|
||||
"key": "site_url",
|
||||
"description": "사이트 URL"
|
||||
},
|
||||
{
|
||||
"key": "recipient_email",
|
||||
"description": "수신자 이메일"
|
||||
}
|
||||
],
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce",
|
||||
"is_active": true,
|
||||
"is_default": true,
|
||||
"user_overrides": null,
|
||||
"templates": [
|
||||
{
|
||||
"id": 1,
|
||||
"definition_id": 1,
|
||||
"channel": "mail",
|
||||
"subject": {
|
||||
"ko": "[{app_name}] 결제 본인 확인 인증 코드",
|
||||
"en": "[{app_name}] Checkout Verification Code"
|
||||
},
|
||||
"body": {
|
||||
"ko": "<h1>결제 본인 확인</h1><p>결제를 진행하기 위해 본인 확인이 필요합니다. 아래 인증 코드를 입력해 주세요.</p><p style=\"font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;\">{code}</p><p>이 코드는 <strong>{expire_minutes}분</strong> 후 만료됩니다.</p><p><strong>본인이 결제를 진행하지 않았다면 이 메일을 무시하고 즉시 비밀번호를 변경해 주세요.</strong></p><p>감사합니다,<br><a href=\"{site_url}\">{app_name}</a></p>",
|
||||
"en": "<h1>Checkout Verification</h1><p>Please enter the code below to proceed with payment.</p><p style=\"font-size:28px; font-weight:bold; letter-spacing:4px; text-align:center; padding:16px; background:#f4f6f8; border-radius:6px;\">{code}</p><p>This code will expire in <strong>{expire_minutes} minutes</strong>.</p><p><strong>If you did not initiate this payment, please ignore this email and change your password immediately.</strong></p><p>Thank you,<br><a href=\"{site_url}\">{app_name}</a></p>"
|
||||
},
|
||||
"is_active": true,
|
||||
"is_default": true,
|
||||
"user_overrides": null,
|
||||
"updated_by": null,
|
||||
"created_at": "2026-07-08 10:43:32",
|
||||
"updated_at": "2026-07-08 10:43:32",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-08 10:43:32",
|
||||
"updated_at": "2026-07-08 10:43:32",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": false
|
||||
}
|
||||
}
|
||||
"...": "(14개 키 생략, 총 19개)"
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"id": 2,
|
||||
"provider_id": "g7:core.mail",
|
||||
"scope_type": "provider_default",
|
||||
"scope_value": "",
|
||||
"...": "(14개 키 생략, 총 19개)"
|
||||
},
|
||||
"... (총 6건 중 2건 표시)"
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"per_page": 25,
|
||||
"total": 1,
|
||||
"total": 6,
|
||||
"from": 1,
|
||||
"to": 1,
|
||||
"has_more_pages": false
|
||||
"...": "(2개 키 생략, 총 7개)"
|
||||
},
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
@@ -368,7 +357,7 @@ HTTP/1.1 200
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** 본인인증 알림 메시지 정의(프로바이더별·목적별·정책별 메일/SMS 템플릿 묶음) 목록을 필터·검색하여 페이지네이션 조회합니다. `auth:sanctum` + `core.admin.identity.messages.read` 관리자 권한이 필요합니다. 확장이 `core.identity.message_definition.index_validation_rules` 필터 훅으로 추가 검색 파라미터를 등록할 수 있습니다. 응답 각 항목의 `user_overrides` 로 운영자가 시드 기본값에서 수정한 필드를, `templates` 로 채널별 하위 템플릿을 함께 내려 관리자 메시지 설정 화면을 구성할 때 사용합니다.
|
||||
**설명** 본인인증 알림 메시지 정의(프로바이더별·목적별·정책별 메일/SMS 템플릿 묶음) 목록을 필터·검색하여 페이지네이션 조회합니다. `auth:sanctum` + `core.admin.identity.messages.read` 관리자 권한이 필요합니다. 확장이 `core.identity.message_definition.index_validation_rules` 필터 훅으로 추가 검색 파라미터를 등록할 수 있습니다. 응답 각 항목의 `user_overrides` 로 운영자가 시드 기본값에서 수정한 필드를 함께 내려 관리자 메시지 설정 화면을 구성할 때 사용합니다. 목록의 `templates` 에는 **대표 템플릿 1건만** 실리고, 전체 채널 수는 `templates_count` 로 제공합니다 — 정의마다 채널×로케일 템플릿 본문을 전부 실으면 목록을 여는 것만으로 전 행의 메일 본문이 전송되기 때문입니다. 채널별 템플릿 전체가 필요하면 단건 조회(`GET /api/admin/identity/messages/definitions/{definition}`)를 사용합니다.
|
||||
|
||||
|
||||
### POST /api/admin/identity/messages/definitions
|
||||
@@ -425,7 +414,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -467,7 +456,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -515,20 +504,20 @@ _단건 응답: `data` 객체의 필드._
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| provider_id | string | `g7:core.mail` | IDV 프로바이더 ID (예: g7:core.mail, kcp, portone) |
|
||||
| scope_type | string | `provider_default` | 메시지 정의 스코프 (provider_default\|purpose\|policy) — App\Enums\IdentityMessageScopeType enum |
|
||||
| scope_value | string | `` | 범위 값: provider_default 빈 문자열 / purpose 키 / policy 키 |
|
||||
| name | object | `{"ko":"메일 본인 확인 (기본)","en":"Mail Verification (default)",…` | 다국어 표시명 ({"ko":"...", "en":"..."}) |
|
||||
| description | object | `{"ko":"특정 목적이 매칭되지 않을 때 사용되는 기본 메일 템플릿","en":"Fallback ma…` | 다국어 설명 |
|
||||
| scope_type | string | `purpose` | 메시지 정의 스코프 (provider_default\|purpose\|policy) — App\Enums\IdentityMessageScopeType enum |
|
||||
| scope_value | string | `checkout_verification` | 범위 값: provider_default 빈 문자열 / purpose 키 / policy 키 |
|
||||
| name | object | `{"ko":"결제 시 본인 확인","en":"Checkout Verification"}` | 다국어 표시명 ({"ko":"...", "en":"..."}) |
|
||||
| description | object | `{"ko":"결제 진행 전 본인\/성인 확인 인증 코드 메일","en":"Identity\/adult …` | 다국어 설명 |
|
||||
| channels | array | `["mail"]` | 활성 채널 (현재 ["mail"], 향후 sms 등 확장) |
|
||||
| variables | array | `[{"key":"code","description":"인증 코드 (text_code 흐름)"},{"ke…` | 사용 가능 변수 메타데이터 ([{key, description}]) |
|
||||
| extension_type | string | `core` | 확장 타입: core, module, plugin |
|
||||
| extension_identifier | string | `core` | 확장 식별자 |
|
||||
| extension_type | string | `module` | 확장 타입: core, module, plugin |
|
||||
| extension_identifier | string | `sirsoft-ecommerce` | 확장 식별자 |
|
||||
| is_active | boolean | `true` | active 여부 |
|
||||
| is_default | boolean | `true` | default 여부 |
|
||||
| user_overrides | array | `["name.ja"]` | 운영자가 수정한 필드명 목록 (예: ["name","is_active"]) |
|
||||
| user_overrides | null | `null` | 운영자가 수정한 필드명 목록 (예: ["name","is_active"]) |
|
||||
| templates | array | `[{"id":1,"definition_id":1,"channel":"mail","subject":{"k…` | 이 정의에 속한 채널별 하위 메시지 템플릿 목록 (원소 id/channel/subject/body 등) |
|
||||
| created_at | string | `2026-05-27 15:20:18` | 생성 일시 |
|
||||
| updated_at | string | `2026-06-30 13:33:16` | 최종 수정 일시 |
|
||||
| created_at | string | `2026-07-30 18:47:09` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-30 18:47:09` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -605,16 +594,16 @@ HTTP/1.1 200
|
||||
"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",
|
||||
"created_at": "2026-07-30 18:47:09",
|
||||
"updated_at": "2026-07-30 18:47:09",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-08 10:43:32",
|
||||
"updated_at": "2026-07-08 10:43:32",
|
||||
"created_at": "2026-07-30 18:47:09",
|
||||
"updated_at": "2026-07-30 18:47:09",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": false
|
||||
@@ -679,7 +668,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -740,8 +729,8 @@ _단건 응답: `data` 객체의 필드._
|
||||
| is_default | boolean | `true` | default 여부 |
|
||||
| user_overrides | null | `null` | 운영자가 수정한 필드명 목록 (예: ["name","is_active"]) |
|
||||
| templates | array | `[{"id":1,"definition_id":1,"channel":"mail","subject":{"k…` | 템플릿 목록 (각 원소 identifier/name 등 — 템플릿 관계 파생) |
|
||||
| created_at | string | `2026-07-08 10:43:32` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 10:43:32` | 최종 수정 일시 |
|
||||
| created_at | string | `2026-07-30 18:47:09` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-30 18:47:09` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -818,16 +807,16 @@ HTTP/1.1 200
|
||||
"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",
|
||||
"created_at": "2026-07-30 18:47:09",
|
||||
"updated_at": "2026-07-30 18:47:09",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-08 10:43:32",
|
||||
"updated_at": "2026-07-08 10:43:32",
|
||||
"created_at": "2026-07-30 18:47:09",
|
||||
"updated_at": "2026-07-30 18:47:09",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": false
|
||||
@@ -889,9 +878,8 @@ _단건 응답: `data` 객체의 필드._
|
||||
| is_active | boolean | `false` | active 여부 |
|
||||
| is_default | boolean | `true` | default 여부 |
|
||||
| user_overrides | array | `["is_active"]` | 운영자가 수정한 필드명 목록 (예: ["name","is_active"]) |
|
||||
| templates | null | `null` | 템플릿 목록 (각 원소 identifier/name 등 — 템플릿 관계 파생) |
|
||||
| created_at | string | `2026-07-08 10:43:32` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 12:14:36` | 최종 수정 일시 |
|
||||
| created_at | string | `2026-07-30 18:47:09` | 생성 일시 |
|
||||
| updated_at | string | `2026-08-04 21:53:38` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":false}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -953,9 +941,8 @@ HTTP/1.1 200
|
||||
"user_overrides": [
|
||||
"is_active"
|
||||
],
|
||||
"templates": null,
|
||||
"created_at": "2026-07-08 10:43:32",
|
||||
"updated_at": "2026-07-08 12:14:36",
|
||||
"created_at": "2026-07-30 18:47:09",
|
||||
"updated_at": "2026-08-04 21:53:38",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": false
|
||||
@@ -1011,7 +998,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1069,7 +1056,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1125,8 +1112,8 @@ _단건 응답: `data` 객체의 필드._
|
||||
| is_default | boolean | `true` | default 여부 |
|
||||
| user_overrides | null | `null` | 운영자가 수정한 필드명 목록 (예: ["subject","body","is_active"]) |
|
||||
| updated_by | null | `null` | 최종 수정한 사용자 정보 (uuid/name — updated_by 관계 파생, 없으면 null) |
|
||||
| created_at | string | `2026-07-08 10:43:32` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 10:43:32` | 최종 수정 일시 |
|
||||
| created_at | string | `2026-07-30 18:47:09` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-30 18:47:09` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -1155,8 +1142,8 @@ HTTP/1.1 200
|
||||
"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",
|
||||
"created_at": "2026-07-30 18:47:09",
|
||||
"updated_at": "2026-07-30 18:47:09",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
@@ -1214,8 +1201,8 @@ _단건 응답: `data` 객체의 필드._
|
||||
| is_default | boolean | `true` | default 여부 |
|
||||
| user_overrides | array | `["is_active"]` | 운영자가 수정한 필드명 목록 (예: ["subject","body","is_active"]) |
|
||||
| updated_by | null | `null` | 최종 수정한 사용자 정보 (uuid/name — updated_by 관계 파생, 없으면 null) |
|
||||
| created_at | string | `2026-07-08 10:43:32` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 12:14:36` | 최종 수정 일시 |
|
||||
| created_at | string | `2026-07-30 18:47:09` | 생성 일시 |
|
||||
| updated_at | string | `2026-08-04 21:53:39` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -1246,8 +1233,8 @@ HTTP/1.1 200
|
||||
"is_active"
|
||||
],
|
||||
"updated_by": null,
|
||||
"created_at": "2026-07-08 10:43:32",
|
||||
"updated_at": "2026-07-08 12:14:36",
|
||||
"created_at": "2026-07-30 18:47:09",
|
||||
"updated_at": "2026-08-04 21:53:39",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
@@ -1304,23 +1291,23 @@ _목록 응답: `data.data[]` 배열 항목의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `25` | 기본 키 (내부 식별자) |
|
||||
| key | string | `test` | 정책 식별자 (고유, 예: core.profile.password_change) |
|
||||
| id | integer | `29` | 기본 키 (내부 식별자) |
|
||||
| key | string | `core.admin.extension_uninstall` | 정책 식별자 (고유, 예: core.profile.password_change) |
|
||||
| scope | string | `route` | 정책 적용 범위 (route: 라우트 패턴 / hook: Service 훅 / custom: 모듈 커스텀 키, IdentityPolicyScope) |
|
||||
| target | string | `sdfsfsf` | 매칭 대상 (scope 에 따라 라우트명/URI 패턴, 훅 이름, 또는 custom key) |
|
||||
| purpose | string | `inicis.adult_verification` | 이 정책이 요구하는 인증 목적 |
|
||||
| provider_id | string | `inicis` | provider 식별자 (연관 리소스 참조) |
|
||||
| target | string | `api.admin.{modules,plugins}.uninstall` | 매칭 대상 (scope 에 따라 라우트명/URI 패턴, 훅 이름, 또는 custom key) |
|
||||
| purpose | string | `sensitive_action` | 이 정책이 요구하는 인증 목적 |
|
||||
| provider_id | null | `null` | provider 식별자 (연관 리소스 참조) |
|
||||
| grace_minutes | integer | `0` | 재인증 유예 시간(분) — 최근 N분 이내 동일 목적 인증 성공 시 재인증 생략 (0=매번 요구) |
|
||||
| enabled | boolean | `true` | 정책 사용 여부 (false 시 인증 미강제) |
|
||||
| enabled | boolean | `false` | 정책 사용 여부 (false 시 인증 미강제) |
|
||||
| priority | integer | `100` | 정책 우선순위 (같은 대상에 여러 정책 매칭 시 작을수록 우선) |
|
||||
| conditions | array | `[]` | 추가 매칭 조건 (역할/HTTP 메서드/파라미터 매칭 조건 JSON, 없으면 빈 배열) |
|
||||
| source_type | string | `admin` | 정책 출처 (core/module/plugin: 선언형 / admin: 운영자 직접 등록, IdentityPolicySourceType) |
|
||||
| source_identifier | string | `sirsoft-board` | 출처 식별자 (선언형 정책의 소유 확장 identifier) |
|
||||
| applies_to | string | `both` | 적용 대상 사용자 (self: 일반 사용자 / admin: 관리자 / both: 모두, IdentityPolicyAppliesTo) |
|
||||
| conditions | object | `{"changed_fields":["email","phone","mobile"]}` | 추가 매칭 조건 (역할/HTTP 메서드/파라미터 매칭 조건 JSON, 없으면 빈 배열) |
|
||||
| source_type | string | `core` | 정책 출처 (core/module/plugin: 선언형 / admin: 운영자 직접 등록, IdentityPolicySourceType) |
|
||||
| source_identifier | string | `core` | 출처 식별자 (선언형 정책의 소유 확장 identifier) |
|
||||
| applies_to | string | `admin` | 적용 대상 사용자 (self: 일반 사용자 / admin: 관리자 / both: 모두, IdentityPolicyAppliesTo) |
|
||||
| fail_mode | string | `block` | 실패 시 동작 (block: HTTP 428 차단 / log_only: 감사 로그만 남기고 통과, IdentityPolicyFailMode) |
|
||||
| user_overrides | array | `[]` | 운영자가 선언 기본값에서 재정의한 필드 목록 (선언형 정책만 의미, 시더 재실행 시 보존) |
|
||||
| created_at | string | `2026-06-26 16:33:04` | 생성 일시 |
|
||||
| updated_at | string | `2026-06-26 16:33:04` | 최종 수정 일시 |
|
||||
| created_at | string | `2026-07-31 00:15:09` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-31 00:15:09` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -1336,52 +1323,52 @@ HTTP/1.1 200
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"id": 12,
|
||||
"key": "sirsoft-board.post.user_create",
|
||||
"scope": "hook",
|
||||
"target": "sirsoft-board.post.before_create",
|
||||
"id": 29,
|
||||
"key": "core.admin.extension_uninstall",
|
||||
"scope": "route",
|
||||
"target": "api.admin.{modules,plugins}.uninstall",
|
||||
"purpose": "sensitive_action",
|
||||
"provider_id": null,
|
||||
"grace_minutes": 0,
|
||||
"enabled": false,
|
||||
"priority": 100,
|
||||
"conditions": null,
|
||||
"source_type": "module",
|
||||
"source_identifier": "sirsoft-board",
|
||||
"applies_to": "self",
|
||||
"source_type": "core",
|
||||
"source_identifier": "core",
|
||||
"applies_to": "admin",
|
||||
"fail_mode": "block",
|
||||
"user_overrides": [],
|
||||
"created_at": "2026-07-08 10:44:35",
|
||||
"updated_at": "2026-07-08 10:44:35",
|
||||
"created_at": "2026-07-31 00:15:09",
|
||||
"updated_at": "2026-07-31 00:15:09",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"key": "sirsoft-board.report.create",
|
||||
"id": 28,
|
||||
"key": "core.admin.user_delete",
|
||||
"scope": "hook",
|
||||
"target": "sirsoft-board.report.before_create",
|
||||
"target": "core.user.before_delete",
|
||||
"purpose": "sensitive_action",
|
||||
"provider_id": null,
|
||||
"grace_minutes": 30,
|
||||
"grace_minutes": 0,
|
||||
"enabled": false,
|
||||
"priority": 100,
|
||||
"conditions": null,
|
||||
"source_type": "module",
|
||||
"source_identifier": "sirsoft-board",
|
||||
"applies_to": "self",
|
||||
"source_type": "core",
|
||||
"source_identifier": "core",
|
||||
"applies_to": "admin",
|
||||
"fail_mode": "block",
|
||||
"user_overrides": [],
|
||||
"created_at": "2026-07-08 10:44:35",
|
||||
"updated_at": "2026-07-08 10:44:35",
|
||||
"created_at": "2026-07-31 00:15:09",
|
||||
"updated_at": "2026-07-31 00:15:09",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
},
|
||||
"... (총 12건 중 2건 표시)"
|
||||
"... (총 21건 중 2건 표시)"
|
||||
],
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
@@ -1391,7 +1378,7 @@ HTTP/1.1 200
|
||||
"meta": {
|
||||
"current_page": 1,
|
||||
"per_page": 25,
|
||||
"total": 12,
|
||||
"total": 21,
|
||||
"last_page": 1
|
||||
}
|
||||
}
|
||||
@@ -1465,7 +1452,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1509,7 +1496,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1582,7 +1569,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1633,7 +1620,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1715,6 +1702,8 @@ HTTP/1.1 200
|
||||
"label": "인증 코드 길이",
|
||||
"type": "integer",
|
||||
"default": 6,
|
||||
"min": 4,
|
||||
"max": 10,
|
||||
"help": "발송되는 숫자 코드의 자릿수 (기본 6, 최소 4, 최대 10)."
|
||||
},
|
||||
"from_address": {
|
||||
@@ -1724,6 +1713,63 @@ HTTP/1.1 200
|
||||
"help": "비어 있으면 시스템 기본 발신자를 사용합니다."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "inicis",
|
||||
"label": "KG이니시스 본인확인",
|
||||
"channels": [
|
||||
"ipin"
|
||||
],
|
||||
"channel_labels": {
|
||||
"ipin": "아이핀"
|
||||
},
|
||||
"render_hint": "text_code",
|
||||
"is_available": true,
|
||||
"abilities": {
|
||||
"can_update": true
|
||||
},
|
||||
"settings_schema": {
|
||||
"is_test_mode": {
|
||||
"label": "테스트 모드",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"test_mid": {
|
||||
"label": "Test MID",
|
||||
"type": "text",
|
||||
"default": "INIiasTest"
|
||||
},
|
||||
"test_api_key": {
|
||||
"label": "Test API Key",
|
||||
"type": "password",
|
||||
"default": "TGdxb2l3enJDWFRTbTgvREU3MGYwUT09"
|
||||
},
|
||||
"live_mid": {
|
||||
"label": "Live MID (SRB prefix)",
|
||||
"type": "text",
|
||||
"default": ""
|
||||
},
|
||||
"live_api_key": {
|
||||
"label": "Live API Key",
|
||||
"type": "password",
|
||||
"default": ""
|
||||
},
|
||||
"duplicate_field": {
|
||||
"label": "Duplicate Field",
|
||||
"type": "radio",
|
||||
"options": [
|
||||
"di",
|
||||
"ci"
|
||||
],
|
||||
"default": "di"
|
||||
},
|
||||
"duplicate_block_enabled": {
|
||||
"label": "중복 가입 차단",
|
||||
"description": "활성화 시 본인인증을 통과한 사람이 이전에 다른 이메일로 가입한 적이 있으면 가입을 거부합니다. 가족 휴대폰 공유 또는 B2B 시나리오 등에서 한 사람이 여러 계정을 가입해야 한다면 비활성화하세요. 이 설정과 무관하게 동일 이메일 재가입은 항상 차단됩니다 (코어 기본 동작).",
|
||||
"type": "toggle",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1778,7 +1824,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1832,7 +1878,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-429 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1865,7 +1911,7 @@ Content-Type: application/json
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/identity/challenges/{challenge} HTTP/1.1
|
||||
GET /api/identity/challenges/006209fd-1b6c-40aa-a38f-9b907f1a4bca HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
@@ -1877,18 +1923,37 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | string | `00484973-8cd3-4a1d-85f2-78361feb6f0d` | 기본 키 (내부 식별자) |
|
||||
| status | string | `verified` | requested\|sent\|processing\|verified\|failed\|expired\|cancelled\|policy_violation_logged |
|
||||
| provider_id | string | `inicis` | 프로바이더 식별자 (예: g7:core.mail, kcp) |
|
||||
| id | string | `006209fd-1b6c-40aa-a38f-9b907f1a4bca` | 기본 키 (내부 식별자) |
|
||||
| status | string | `requested` | requested\|sent\|processing\|verified\|failed\|expired\|cancelled\|policy_violation_logged |
|
||||
| provider_id | string | `g7:core.mail` | 프로바이더 식별자 (예: g7:core.mail, kcp) |
|
||||
| purpose | string | `sensitive_action` | 인증 목적 (signup\|password_reset\|self_update\|sensitive_action\|login\|*module-defined*) — 코어 5종은 App\Enums\IdentityVerificationPurpose enum, 모듈/플러그인은 declaredPurposes 레지스트리 |
|
||||
| render_hint | string | `text_code` | 프론트 렌더 힌트 (text_code\|link\|external_redirect) |
|
||||
| expires_at | string | `2026-05-12T18:14:19+00:00` | expires 일시 |
|
||||
| expires_at | string | `2026-07-20T20:11:14+00:00` | expires 일시 |
|
||||
| max_attempts | integer | `5` | 허용 최대 시도 횟수 (정책 상수). 누적 시도 횟수(attempts)는 잠금 직전까지 시도 횟수를 맞춰 보는 데 쓰일 수 있어 본 응답에 포함하지 않는다 |
|
||||
| public_payload | array | `[]` | 프론트 렌더에 필요한 공개 안전 페이로드 (민감 metadata 제외, 프로바이더별 UI 힌트 — 없으면 빈 배열) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "messages.success",
|
||||
"data": {
|
||||
"id": "006209fd-1b6c-40aa-a38f-9b907f1a4bca",
|
||||
"status": "requested",
|
||||
"provider_id": "g7:core.mail",
|
||||
"purpose": "sensitive_action",
|
||||
"render_hint": "text_code",
|
||||
"expires_at": "2026-07-20T20:11:14+00:00",
|
||||
"max_attempts": 5,
|
||||
"public_payload": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -1916,7 +1981,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/identity/challenges/{challenge}/cancel HTTP/1.1
|
||||
POST /api/identity/challenges/006209fd-1b6c-40aa-a38f-9b907f1a4bca/cancel HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
@@ -1924,11 +1989,11 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-429 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-429 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -1961,7 +2026,7 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/identity/challenges/{challenge}/verify HTTP/1.1
|
||||
POST /api/identity/challenges/006209fd-1b6c-40aa-a38f-9b907f1a4bca/verify HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
|
||||
@@ -1975,11 +2040,11 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-429 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-429 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -2090,6 +2155,21 @@ HTTP/1.1 200
|
||||
"abilities": {
|
||||
"can_update": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "inicis",
|
||||
"label": "KG이니시스 본인확인",
|
||||
"channels": [
|
||||
"ipin"
|
||||
],
|
||||
"channel_labels": {
|
||||
"ipin": "아이핀"
|
||||
},
|
||||
"render_hint": "text_code",
|
||||
"is_available": true,
|
||||
"abilities": {
|
||||
"can_update": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2197,10 +2277,9 @@ HTTP/1.1 200
|
||||
"id": "login",
|
||||
"label": "로그인 2단계 인증",
|
||||
"description": "2단계 인증을 켰을 때 비밀번호 확인 뒤 한 단계를 더 요구.",
|
||||
"default_provider": "g7:core.mail",
|
||||
"default_provider": null,
|
||||
"allowed_channels": [
|
||||
"mail",
|
||||
"sms"
|
||||
"email"
|
||||
],
|
||||
"source_type": "core",
|
||||
"source_identifier": "core"
|
||||
@@ -2217,6 +2296,17 @@ HTTP/1.1 200
|
||||
],
|
||||
"source_type": "module",
|
||||
"source_identifier": "sirsoft-ecommerce"
|
||||
},
|
||||
{
|
||||
"id": "inicis.adult_verification",
|
||||
"label": "성인인증 (KG이니시스 본인확인 전용)",
|
||||
"description": "반드시 KG이니시스 provider 로만 매핑하세요. 메일/SMS provider 는 생년월일을 반환하지 않아 성인 여부 판정이 불가능합니다. 잘못 매핑 시 비성인 사용자에게 19금 컨텐츠가 노출될 수 있습니다.",
|
||||
"default_provider": "inicis",
|
||||
"allowed_channels": [
|
||||
"ipin"
|
||||
],
|
||||
"source_type": "plugin",
|
||||
"source_identifier": "sirsoft-verification_kginicis"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Language Packs 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -52,22 +52,22 @@ _목록 응답: `data.data[]` 배열 항목의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `16` | 기본 키 (내부 식별자) |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| identifier | string | `g7-core-en` | 언어팩 고유 식별자 ({vendor}-{scope}-{target?}-{locale}) |
|
||||
| vendor | string | `g7` | 언어팩 제작자 식별자 |
|
||||
| scope | string | `core` | 적용 대상 분류 |
|
||||
| target_identifier | string | `gnuboard7-hello_module` | 대상 확장 식별자 (scope=core일 때 null) |
|
||||
| target_identifier | string | `apidoc-sample-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` | 언어팩 버전 |
|
||||
| version | string | `7.0.6` | 언어팩 버전 |
|
||||
| latest_version | string | `1.0.0` | 감지된 최신 배포 버전 |
|
||||
| target_version_constraint | null | `null` | 대상 확장 버전 제약 (semver) |
|
||||
| target_version_mismatch | boolean | `false` | 대상 버전 불일치 경고 플래그 |
|
||||
| name | string | `API 문서 샘플 언어팩` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| name | string | `G7 코어 일본어 언어팩` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| license | string | `MIT` | 라이선스 |
|
||||
| description | string | `문서 실측용 언어팩` | 언어팩 설명 (다국어) |
|
||||
| description | string | `G7 코어 일본어 언어팩 (번들)` | 언어팩 설명 (다국어) |
|
||||
| status | string | `active` | 언어팩 상태 |
|
||||
| is_protected | boolean | `true` | protected 여부 |
|
||||
| source_type | string | `built_in` | 설치 소스 유형 (zip/github/url/bundled/bundled_with_extension) |
|
||||
@@ -75,15 +75,15 @@ _목록 응답: `data.data[]` 배열 항목의 필드._
|
||||
| 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) |
|
||||
| bundled_identifier | string | `g7-core-ja` | 대응하는 번들 확장 식별자 (번들 원본 매칭용) |
|
||||
| install_blocked_reason | string | `core_locale_missing` | 설치가 차단된 사유 (차단 없으면 null) |
|
||||
| files_missing | boolean | `false` | 드리프트 여부 — active 로 기록됐으나 설치본 파일이 부재해 런타임에 조용히 ko 로 폴백하는 상태이면 true |
|
||||
| bundled_source_available | boolean | `false` | 번들 소스(`lang-packs/_bundled/{identifier}`)가 실재해 재설치로 복구 가능하면 true. 설치 경로(source_type)와 무관하게 소스 실재 여부로 판정 |
|
||||
| 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` | 최종 수정 일시 |
|
||||
| target_name | string | `Hello 모듈` | 대상 확장의 표시 이름 (scope+target_identifier 로 해석) |
|
||||
| installed_at | string | `2026-07-28 22:55:00` | installed 일시 |
|
||||
| activated_at | string | `2026-08-01 11:36:31` | activated 일시 |
|
||||
| created_at | string | `2026-07-31 22:55:00` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-31 22:55:00` | 최종 수정 일시 |
|
||||
| has_update | boolean | `false` | update 여부 |
|
||||
| abilities | object | `{"can_activate":true,"can_deactivate":true,"can_uninstall…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
@@ -109,7 +109,7 @@ HTTP/1.1 200
|
||||
"locale_name": "EN",
|
||||
"locale_native_name": "English",
|
||||
"text_direction": "ltr",
|
||||
"version": "7.0.1",
|
||||
"version": "7.0.6",
|
||||
"latest_version": null,
|
||||
"target_version_constraint": null,
|
||||
"target_version_mismatch": false,
|
||||
@@ -140,56 +140,54 @@ HTTP/1.1 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"identifier": "apidoc-sample-lang",
|
||||
"vendor": "apidoc",
|
||||
"id": null,
|
||||
"identifier": "g7-core-ja",
|
||||
"vendor": "sirsoft",
|
||||
"scope": "core",
|
||||
"target_identifier": null,
|
||||
"locale": "fr",
|
||||
"locale_name": "French",
|
||||
"locale_native_name": "Français",
|
||||
"locale": "ja",
|
||||
"locale_name": "Japanese",
|
||||
"locale_native_name": "日本語",
|
||||
"text_direction": "ltr",
|
||||
"version": "1.0.0",
|
||||
"latest_version": "1.0.0",
|
||||
"version": "1.0.9",
|
||||
"latest_version": null,
|
||||
"target_version_constraint": null,
|
||||
"target_version_mismatch": false,
|
||||
"name": "API 문서 샘플 언어팩",
|
||||
"name": "G7 코어 일본어 언어팩",
|
||||
"license": "MIT",
|
||||
"description": "문서 실측용 언어팩",
|
||||
"status": "active",
|
||||
"description": "G7 코어 일본어 언어팩 (번들)",
|
||||
"status": "uninstalled",
|
||||
"is_protected": false,
|
||||
"source_type": "bundled",
|
||||
"origin": "bundled",
|
||||
"source_url": null,
|
||||
"source_url": "g7-core-ja",
|
||||
"github_url": null,
|
||||
"github_changelog_url": null,
|
||||
"bundled_identifier": null,
|
||||
"bundled_identifier": "g7-core-ja",
|
||||
"install_blocked_reason": null,
|
||||
"files_missing": false,
|
||||
"bundled_source_available": false,
|
||||
"bundled_source_available": true,
|
||||
"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",
|
||||
"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
|
||||
"can_install": true
|
||||
}
|
||||
},
|
||||
"... (총 25건 중 2건 표시)"
|
||||
],
|
||||
"meta": {
|
||||
"total": 43,
|
||||
"total": 57,
|
||||
"active": 15,
|
||||
"installed": 0,
|
||||
"installed": 1,
|
||||
"inactive": 0,
|
||||
"error": 0,
|
||||
"uninstalled": 10,
|
||||
"uninstalled": 9,
|
||||
"current_page": 1,
|
||||
"last_page": 2,
|
||||
"last_page": 3,
|
||||
"per_page": 25
|
||||
},
|
||||
"abilities": {
|
||||
@@ -250,7 +248,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -290,7 +288,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -338,7 +336,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -393,7 +391,7 @@ Content-Disposition: form-data; name="auto_activate"
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -442,7 +440,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -475,6 +473,8 @@ Content-Type: application/json
|
||||
| checksum | body | string | 아니오 | — | 무결성 검증 체크섬 (SHA-256) |
|
||||
| auto_activate | body | boolean | 아니오 | — | 설치 후 자동 활성화 여부. 외부 소스(파일·URL·GitHub)에서 설치할 때 이 값을 참으로 보내려면 `core.language_packs.manage` 권한이 함께 필요하며, 없으면 이 항목만 422 로 거부됩니다 — 항목을 빼면 설치는 그대로 진행되고 상태는 `installed` 로 남습니다. 설치(install)와 활성화(manage)를 별도 권한으로 두는 정책을 따르기 위함입니다 |
|
||||
|
||||
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.language_packs.install_from_url_validation_rules`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
@@ -493,7 +493,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -543,7 +543,7 @@ Content-Type: application/octet-stream
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -583,7 +583,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -617,7 +617,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/admin/language-packs/1?cascade=1 HTTP/1.1
|
||||
DELETE /api/admin/language-packs/{id}?cascade=1 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -625,7 +625,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -660,7 +660,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/language-packs/1 HTTP/1.1
|
||||
GET /api/admin/language-packs/{id} HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -668,117 +668,11 @@ 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 | `[]` | 변경 이력 항목 목록 (버전별 변경 내용) |
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```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": []
|
||||
}
|
||||
}
|
||||
```
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -808,7 +702,7 @@ HTTP/1.1 200
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/1/activate HTTP/1.1
|
||||
POST /api/admin/language-packs/{id}/activate HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -816,7 +710,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -850,7 +744,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/language-packs/1/changelog HTTP/1.1
|
||||
GET /api/admin/language-packs/{id}/changelog HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -858,33 +752,11 @@ 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 여부 |
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "언어팩 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"identifier": "apidoc-sample-lang",
|
||||
"entries": [],
|
||||
"changelog": "",
|
||||
"has_changelog": false
|
||||
}
|
||||
}
|
||||
```
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -914,7 +786,7 @@ HTTP/1.1 200
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/1/deactivate HTTP/1.1
|
||||
POST /api/admin/language-packs/{id}/deactivate HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -922,7 +794,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -956,7 +828,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
POST /api/admin/language-packs/1/update HTTP/1.1
|
||||
POST /api/admin/language-packs/{id}/update HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -964,7 +836,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Layouts 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -17,6 +17,46 @@
|
||||
---
|
||||
|
||||
|
||||
### GET /api/layouts/preview/{token}
|
||||
<!-- @generated:start:api.public.layouts.preview.serve.extensionless -->
|
||||
- **라우트명**: `api.public.layouts.preview.serve.extensionless`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Public\LayoutPreviewController@serve`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| token | path | string | 예 | — | 인증/검증 토큰 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/layouts/preview/{token} 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 -->
|
||||
|
||||
**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 -->
|
||||
|
||||
|
||||
### GET /api/layouts/preview/{token}.json
|
||||
<!-- @generated:start:api.public.layouts.preview.serve -->
|
||||
- **라우트명**: `api.public.layouts.preview.serve`
|
||||
@@ -57,6 +97,47 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
|
||||
**설명** 편집 중인 레이아웃을 미리보기 토큰(UUID)으로 조회해 JSON으로 서빙합니다. 토큰으로 대상 레이아웃을 찾은 뒤 상속 병합과 확장(extension) 적용까지 마친 결과를 반환하며, 토큰이 유효하지 않으면 404를 반환합니다. 인증 미들웨어가 적용되지만 실질적 보안 메커니즘은 토큰 자체이며, 레이아웃 편집기에서 저장 전 변경분을 실제 렌더링으로 확인하는 용도입니다.
|
||||
|
||||
|
||||
### GET /api/layouts/{templateIdentifier}/{layoutName}
|
||||
<!-- @generated:start:api.public.layouts.serve.extensionless -->
|
||||
- **라우트명**: `api.public.layouts.serve.extensionless`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicLayoutController@serve`
|
||||
- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| templateIdentifier | path | string | 예 | — | 대상 template의 식별자 |
|
||||
| layoutName | path | string | 예 | — | 대상 layout의 이름 (식별자) |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/layouts/{templateIdentifier}/{layoutName} 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 -->
|
||||
|
||||
**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 -->
|
||||
|
||||
|
||||
### GET /api/layouts/{templateIdentifier}/{layoutName}.json
|
||||
<!-- @generated:start:api.public.layouts.serve -->
|
||||
- **라우트명**: `api.public.layouts.serve`
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 License 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Locales 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -41,8 +41,8 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| locales | array | `["ko","en","fr","ja"]` | 활성 로케일 코드 배열 |
|
||||
| locale_names | object | `{"ko":"한국어","en":"English","ja":"日本語","fr":"Français"}` | 로케일 코드별 표시명 맵 (config app.locale_names) |
|
||||
| locales | array | `["ko","en"]` | 활성 로케일 코드 배열 |
|
||||
| locale_names | object | `{"ko":"한국어","en":"English"}` | 로케일 코드별 표시명 맵 (config app.locale_names) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -57,13 +57,11 @@ HTTP/1.1 200
|
||||
"data": {
|
||||
"locales": [
|
||||
"ko",
|
||||
"en",
|
||||
"fr"
|
||||
"en"
|
||||
],
|
||||
"locale_names": {
|
||||
"ko": "한국어",
|
||||
"en": "English",
|
||||
"fr": "Français"
|
||||
"en": "English"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+73
-39
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Me 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -38,7 +38,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-500 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -82,31 +82,31 @@ _단건 응답: `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` | 이메일 주소 |
|
||||
| uuid | string | `a26219fc-94a0-4f63-9404-04c2a6ac99e4` | 외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) |
|
||||
| name | string | `최고관리자` | 사용자 이름 |
|
||||
| nickname | string | `최고관리자` | 닉네임 |
|
||||
| email | string | `heuristing@gmail.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) |
|
||||
| country | null | `null` | 국가 코드 (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 여부 |
|
||||
| homepage | null | `null` | 홈페이지 URL |
|
||||
| mobile | null | `null` | 휴대폰 번호 |
|
||||
| phone | null | `null` | 전화번호 |
|
||||
| zipcode | null | `null` | 우편번호 |
|
||||
| address | null | `null` | 기본 주소 |
|
||||
| address_detail | null | `null` | 상세 주소 |
|
||||
| signature | null | `null` | 서명 |
|
||||
| bio | null | `null` | 자기소개 |
|
||||
| is_super | boolean | `true` | 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` | 생성 일시 |
|
||||
| last_login_at | string | `2026-08-04 19:00:10` | last login 일시 |
|
||||
| last_login_human | string | `2시간 전` | 마지막 로그인 시각의 상대 표현 (diffForHumans() 산물, 사용자 시간대 기준) |
|
||||
| created_at | string | `2026-07-30 23:37:44` | 생성 일시 |
|
||||
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
|
||||
| notify_post_complete | boolean | `false` | 게시글 작성 완료 알림 수신 설정 (게시판 모듈 주입) |
|
||||
| notify_post_reply | boolean | `false` | 내 게시글에 대한 답글 알림 수신 설정 (게시판 모듈 주입) |
|
||||
@@ -150,36 +150,66 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "프로필 정보를 성공적으로 가져왔습니다.",
|
||||
"data": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 사용자",
|
||||
"nickname": "song.hyunji",
|
||||
"email": "apidoc-sample-user@example.com",
|
||||
"uuid": "a26219fc-94a0-4f63-9404-04c2a6ac99e4",
|
||||
"name": "최고관리자",
|
||||
"nickname": "최고관리자",
|
||||
"email": "heuristing@gmail.com",
|
||||
"avatar": null,
|
||||
"language": "ko",
|
||||
"timezone": "Asia/Seoul",
|
||||
"country": "KR",
|
||||
"country": null,
|
||||
"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,
|
||||
"homepage": null,
|
||||
"mobile": null,
|
||||
"phone": null,
|
||||
"zipcode": null,
|
||||
"address": null,
|
||||
"address_detail": null,
|
||||
"signature": null,
|
||||
"bio": null,
|
||||
"is_super": true,
|
||||
"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",
|
||||
"last_login_at": "2026-08-04 19:00:10",
|
||||
"last_login_human": "2시간 전",
|
||||
"created_at": "2026-07-30 23:37:44",
|
||||
"is_owner": true,
|
||||
"notify_post_complete": false,
|
||||
"notify_post_reply": false,
|
||||
"notify_comment": false,
|
||||
"notify_reply_comment": false,
|
||||
"email_subscription": false,
|
||||
"email_subscription_at": null,
|
||||
"marketing_consent": false,
|
||||
"marketing_consent_at": null,
|
||||
"third_party_consent": false,
|
||||
"third_party_consent_at": null,
|
||||
"info_disclosure": false,
|
||||
"info_disclosure_at": null,
|
||||
"marketing_consent_enabled": true,
|
||||
"marketing_consent_terms_slug": "marketing-terms",
|
||||
"marketing_consent_terms_slug_set": true,
|
||||
"third_party_consent_enabled": true,
|
||||
"third_party_consent_terms_slug": null,
|
||||
"third_party_consent_terms_slug_set": false,
|
||||
"info_disclosure_enabled": true,
|
||||
"info_disclosure_terms_slug": null,
|
||||
"info_disclosure_terms_slug_set": false,
|
||||
"email_subscription_enabled": true,
|
||||
"email_subscription_terms_slug": null,
|
||||
"email_subscription_terms_slug_set": false,
|
||||
"channels": [
|
||||
{
|
||||
"key": "email_subscription",
|
||||
"label": "광고성 이메일 수신",
|
||||
"enabled": true,
|
||||
"terms_slug": null,
|
||||
"terms_slug_set": false
|
||||
}
|
||||
],
|
||||
"consent_histories": [],
|
||||
"ecommerce_mileage": {
|
||||
"enabled": false
|
||||
},
|
||||
@@ -269,13 +299,17 @@ Content-Type: application/json
|
||||
"notify_post_complete": true,
|
||||
"notify_post_reply": true,
|
||||
"notify_comment": true,
|
||||
"notify_reply_comment": true
|
||||
"notify_reply_comment": true,
|
||||
"email_subscription": true,
|
||||
"marketing_consent": true,
|
||||
"third_party_consent": true,
|
||||
"info_disclosure": true
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
+235
-502
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Menus 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -49,7 +49,7 @@ _목록 응답: `data.data[]` 배열 항목의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| id | integer | `19` | 기본 키 (내부 식별자) |
|
||||
| name | object | `{"ko":"대시보드","en":"Dashboard","ja":"ダッシュボード"}` | 메뉴 이름 (다국어 JSON) |
|
||||
| slug | string | `admin-dashboard` | 메뉴 슬러그 |
|
||||
| url | string | `/admin/dashboard` | 메뉴 URL |
|
||||
@@ -61,9 +61,9 @@ _목록 응답: `data.data[]` 배열 항목의 필드._
|
||||
| extension_identifier | string | `core` | 확장 식별자 (예: core, sirsoft-board, sirsoft-payment) |
|
||||
| children | array | `[]` | 하위 항목 배열 (계층 트리 — children 관계 파생) |
|
||||
| creator | null | `null` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) |
|
||||
| roles | array | `[{"id":1,"name":{"ko":"관리자","en":"Administrator"},"permis…` | 이 메뉴 노출이 허용된 역할 목록 (원소 id/name/permission_type — roles 관계 파생, permission_type 은 pivot 의 노출 권한 유형) |
|
||||
| created_at | string | `2026-05-27 15:20:18` | 생성 일시 |
|
||||
| updated_at | string | `2026-05-27 15:21:38` | 최종 수정 일시 |
|
||||
| roles | array | `[{"id":1,"name":{"ko":"관리자","en":"Administrator","ja":"管理…` | 이 메뉴 노출이 허용된 역할 목록 (원소 id/name/permission_type — roles 관계 파생, permission_type 은 pivot 의 노출 권한 유형) |
|
||||
| created_at | string | `2026-07-31 00:13:57` | 생성 일시 |
|
||||
| updated_at | string | `2026-08-01 11:46:14` | 최종 수정 일시 |
|
||||
| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
|
||||
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
@@ -82,17 +82,18 @@ HTTP/1.1 200
|
||||
{
|
||||
"id": 19,
|
||||
"name": {
|
||||
"ko": "페이지 관리",
|
||||
"en": "Page Management"
|
||||
"ko": "대시보드",
|
||||
"en": "Dashboard",
|
||||
"ja": "ダッシュボード"
|
||||
},
|
||||
"slug": "sirsoft-page",
|
||||
"url": "/admin/pages",
|
||||
"icon": "fas fa-file-alt",
|
||||
"order": 25,
|
||||
"slug": "admin-dashboard",
|
||||
"url": "/admin/dashboard",
|
||||
"icon": "fas fa-tachometer-alt",
|
||||
"order": 1,
|
||||
"is_active": true,
|
||||
"parent_id": null,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-page",
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "core",
|
||||
"children": [],
|
||||
"creator": null,
|
||||
"roles": [
|
||||
@@ -100,13 +101,23 @@ HTTP/1.1 200
|
||||
"id": 1,
|
||||
"name": {
|
||||
"ko": "관리자",
|
||||
"en": "Administrator"
|
||||
"en": "Administrator",
|
||||
"ja": "管理者"
|
||||
},
|
||||
"permission_type": "read"
|
||||
},
|
||||
{
|
||||
"id": 156,
|
||||
"name": {
|
||||
"ko": "매니저",
|
||||
"en": "Manager",
|
||||
"ja": "マネージャー"
|
||||
},
|
||||
"permission_type": "read"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-08 10:44:43",
|
||||
"updated_at": "2026-07-08 10:44:43",
|
||||
"created_at": "2026-07-31 00:13:57",
|
||||
"updated_at": "2026-08-01 11:46:14",
|
||||
"is_owner": false,
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
@@ -115,109 +126,35 @@ HTTP/1.1 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"id": 20,
|
||||
"name": {
|
||||
"ko": "게시판 관리",
|
||||
"en": "Board Management"
|
||||
"ko": "환경설정",
|
||||
"en": "Settings",
|
||||
"ja": "環境設定"
|
||||
},
|
||||
"slug": "sirsoft-board",
|
||||
"url": null,
|
||||
"icon": "fas fa-clipboard-list",
|
||||
"order": 30,
|
||||
"slug": "admin-settings",
|
||||
"url": "/admin/settings",
|
||||
"icon": "fas fa-cog",
|
||||
"order": 2,
|
||||
"is_active": true,
|
||||
"parent_id": null,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board",
|
||||
"children": [
|
||||
{
|
||||
"id": 16,
|
||||
"name": {
|
||||
"ko": "환경설정",
|
||||
"en": "Settings"
|
||||
},
|
||||
"slug": "sirsoft-board-settings",
|
||||
"url": "/admin/boards/settings",
|
||||
"icon": "fas fa-cog",
|
||||
"order": 1,
|
||||
"is_active": true,
|
||||
"parent_id": 15,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board",
|
||||
"roles": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": {
|
||||
"ko": "관리자",
|
||||
"en": "Administrator"
|
||||
},
|
||||
"permission_type": "read"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": {
|
||||
"ko": "게시판 목록",
|
||||
"en": "Board List"
|
||||
},
|
||||
"slug": "sirsoft-board-list",
|
||||
"url": "/admin/boards",
|
||||
"icon": "fas fa-list",
|
||||
"order": 2,
|
||||
"is_active": true,
|
||||
"parent_id": 15,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board",
|
||||
"roles": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": {
|
||||
"ko": "관리자",
|
||||
"en": "Administrator"
|
||||
},
|
||||
"permission_type": "read"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": {
|
||||
"ko": "게시판 신고현황",
|
||||
"en": "Board Reports"
|
||||
},
|
||||
"slug": "sirsoft-board-reports",
|
||||
"url": "/admin/boards/reports",
|
||||
"icon": "fas fa-flag",
|
||||
"order": 3,
|
||||
"is_active": true,
|
||||
"parent_id": 15,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board",
|
||||
"roles": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": {
|
||||
"ko": "관리자",
|
||||
"en": "Administrator"
|
||||
},
|
||||
"permission_type": "read"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "core",
|
||||
"children": [],
|
||||
"creator": null,
|
||||
"roles": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": {
|
||||
"ko": "관리자",
|
||||
"en": "Administrator"
|
||||
"en": "Administrator",
|
||||
"ja": "管理者"
|
||||
},
|
||||
"permission_type": "read"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-08 10:44:35",
|
||||
"updated_at": "2026-07-08 10:44:35",
|
||||
"created_at": "2026-07-31 00:13:57",
|
||||
"updated_at": "2026-08-01 11:46:14",
|
||||
"is_owner": false,
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
@@ -225,7 +162,7 @@ HTTP/1.1 200
|
||||
"can_delete": true
|
||||
}
|
||||
},
|
||||
"... (총 3건 중 2건 표시)"
|
||||
"... (총 17건 중 2건 표시)"
|
||||
],
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
@@ -248,7 +185,7 @@ HTTP/1.1 200
|
||||
|
||||
**설명**
|
||||
|
||||
관리자 메뉴 관리 화면(`partials/admin_menu_list/*`)의 목록 표시 기준 엔드포인트. `is_active` 또는 `filters` 가 있으면 필터링된 관리용 메뉴를, 없으면 최상위 메뉴 전체를 반환한다. `filters` 는 `field`(name/slug/url/all)·`value`·`operator`(like/eq/starts_with/ends_with) 조합의 배열이며 최대 10개까지 허용된다. `name` 은 다국어 JSON 객체로 내려오고, 각 항목에 `children`/`creator`/`roles` 관계와 `abilities`(현재 사용자의 수정/삭제 가능 여부)가 포함된다. 인증 계약: `auth:sanctum` + `permission:core.menus.read`.
|
||||
관리자 메뉴 관리 화면(`partials/admin_menu_list/*`)의 목록 표시 기준 엔드포인트. `is_active` 또는 `filters` 가 있으면 필터링된 관리용 메뉴를, 없으면 최상위 메뉴 전체를 반환한다. `filters` 는 `field`(name/slug/url/all)·`value`·`operator`(like/eq/starts_with/ends_with) 조합의 배열이며 최대 10개까지 허용된다. `name` 은 다국어 JSON 객체로 내려오고, 각 항목에 `children`/`creator`/`roles` 관계와 `abilities`(현재 사용자의 수정/삭제 가능 여부)가 포함된다. 단, **하위 항목(`children[]`)에는 `roles` 키 자체가 없다** — 상위 메뉴마다 하위 전체의 역할 피벗까지 싣지 않기 위해서다. 빈 배열이 아니라 키를 생략하는 이유는, 빈 배열이 "역할 제한이 없다" 는 사실이 아닌 단언이 되어 그 값을 그대로 수정 요청에 실어 보내면 해당 메뉴의 역할 제한이 통째로 해제되기 때문이다. 하위 메뉴의 역할이 필요하면 `GET /api/admin/menus/{menu}` 로 단건 조회한다 (단건 응답은 항상 `roles` 를 포함한다). 인증 계약: `auth:sanctum` + `permission:core.menus.read`.
|
||||
|
||||
|
||||
### POST /api/admin/menus
|
||||
@@ -305,22 +242,22 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `26` | 기본 키 (내부 식별자) |
|
||||
| name | object | `{"ko":"실측 예시값","en":"실측 예시값","fr":"실측 예시값"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| slug | string | `probe_6a4dc09f5a6bd` | URL 친화 식별자 (slug) |
|
||||
| id | integer | `39` | 기본 키 (내부 식별자) |
|
||||
| name | object | `{"ko":"실측 예시값","en":"실측 예시값"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| slug | string | `probe_6a71e0d4afdbc` | URL 친화 식별자 (slug) |
|
||||
| url | string | `https://example.com` | 메뉴 URL |
|
||||
| icon | string | `실측 예시값` | 아이콘 식별자 (아이콘 클래스/이름) |
|
||||
| order | integer | `1` | 메뉴 순서 |
|
||||
| is_active | boolean | `true` | active 여부 |
|
||||
| parent_id | integer | `1` | parent 식별자 (연관 리소스 참조) |
|
||||
| extension_type | string | `core` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) |
|
||||
| extension_identifier | string | `probe_6a4dc09f5a6c3` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| parent | object | `{"id":1,"name":{"ko":"API 문서 샘플 메뉴","en":"API Doc Sample …` | 상위 항목 객체 (parent 관계 파생) |
|
||||
| extension_identifier | string | `probe_6a71e0d4afdc2` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| parent | object | `{"id":1,"name":{"ko":"게시판 관리","en":"Board Management"},"u…` | 상위 항목 객체 (parent 관계 파생) |
|
||||
| children | array | `[]` | 하위 항목 배열 (계층 트리 — children 관계 파생) |
|
||||
| creator | null | `null` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) |
|
||||
| roles | array | `[{"id":1,"name":{"ko":"관리자","en":"Administrator"},"permis…` | 보유 역할 목록 (각 원소 id/name/permissions — roles 관계 파생) |
|
||||
| created_at | string | `2026-07-08 12:14:39` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 12:14:39` | 최종 수정 일시 |
|
||||
| roles | array | `[{"id":1,"name":{"ko":"관리자","en":"Administrator","ja":"管理…` | 보유 역할 목록 (각 원소 id/name/permissions — roles 관계 파생) |
|
||||
| created_at | string | `2026-08-04 21:53:40` | 생성 일시 |
|
||||
| updated_at | string | `2026-08-04 21:53:40` | 최종 수정 일시 |
|
||||
| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
|
||||
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
@@ -335,28 +272,27 @@ HTTP/1.1 201
|
||||
"success": true,
|
||||
"message": "메뉴가 성공적으로 생성되었습니다.",
|
||||
"data": {
|
||||
"id": 26,
|
||||
"id": 39,
|
||||
"name": {
|
||||
"ko": "실측 예시값",
|
||||
"en": "실측 예시값",
|
||||
"fr": "실측 예시값"
|
||||
"en": "실측 예시값"
|
||||
},
|
||||
"slug": "probe_6a4dc09f5a6bd",
|
||||
"slug": "probe_6a71e0d4afdbc",
|
||||
"url": "https://example.com",
|
||||
"icon": "실측 예시값",
|
||||
"order": 1,
|
||||
"is_active": true,
|
||||
"parent_id": 1,
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "probe_6a4dc09f5a6c3",
|
||||
"extension_identifier": "probe_6a71e0d4afdc2",
|
||||
"parent": {
|
||||
"id": 1,
|
||||
"name": {
|
||||
"ko": "API 문서 샘플 메뉴",
|
||||
"en": "API Doc Sample Menu"
|
||||
"ko": "게시판 관리",
|
||||
"en": "Board Management"
|
||||
},
|
||||
"url": "/admin/apidoc-sample",
|
||||
"icon": "fas fa-book"
|
||||
"url": null,
|
||||
"icon": "fas fa-clipboard-list"
|
||||
},
|
||||
"children": [],
|
||||
"creator": null,
|
||||
@@ -365,21 +301,14 @@ HTTP/1.1 201
|
||||
"id": 1,
|
||||
"name": {
|
||||
"ko": "관리자",
|
||||
"en": "Administrator"
|
||||
},
|
||||
"permission_type": "read"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": {
|
||||
"ko": "API 문서 샘플 역할",
|
||||
"en": "API Doc Sample Role"
|
||||
"en": "Administrator",
|
||||
"ja": "管理者"
|
||||
},
|
||||
"permission_type": "read"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-08 12:14:39",
|
||||
"updated_at": "2026-07-08 12:14:39",
|
||||
"created_at": "2026-08-04 21:53:40",
|
||||
"updated_at": "2026-08-04 21:53:40",
|
||||
"is_owner": false,
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
@@ -428,7 +357,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| id | integer | `19` | 기본 키 (내부 식별자) |
|
||||
| name | object | `{"ko":"대시보드","en":"Dashboard","ja":"ダッシュボード"}` | 메뉴 이름 (다국어 JSON) |
|
||||
| slug | string | `admin-dashboard` | 메뉴 슬러그 |
|
||||
| url | string | `/admin/dashboard` | 메뉴 URL |
|
||||
@@ -451,254 +380,64 @@ HTTP/1.1 200
|
||||
{
|
||||
"id": 19,
|
||||
"name": {
|
||||
"ko": "페이지 관리",
|
||||
"en": "Page Management"
|
||||
"ko": "대시보드",
|
||||
"en": "Dashboard",
|
||||
"ja": "ダッシュボード"
|
||||
},
|
||||
"slug": "sirsoft-page",
|
||||
"url": "/admin/pages",
|
||||
"icon": "fas fa-file-alt",
|
||||
"order": 25,
|
||||
"is_active": true,
|
||||
"children": []
|
||||
"slug": "admin-dashboard",
|
||||
"url": "/admin/dashboard",
|
||||
"icon": "fas fa-tachometer-alt",
|
||||
"...": "(3개 키 생략, 총 8개)"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"id": 20,
|
||||
"name": {
|
||||
"ko": "게시판 관리",
|
||||
"en": "Board Management"
|
||||
"ko": "환경설정",
|
||||
"en": "Settings",
|
||||
"ja": "環境設定"
|
||||
},
|
||||
"slug": "sirsoft-board",
|
||||
"url": null,
|
||||
"icon": "fas fa-clipboard-list",
|
||||
"order": 30,
|
||||
"is_active": true,
|
||||
"children": [
|
||||
{
|
||||
"id": 16,
|
||||
"name": {
|
||||
"ko": "환경설정",
|
||||
"en": "Settings"
|
||||
},
|
||||
"slug": "sirsoft-board-settings",
|
||||
"url": "/admin/boards/settings",
|
||||
"icon": "fas fa-cog",
|
||||
"order": 1,
|
||||
"is_active": true,
|
||||
"parent_id": 15,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": {
|
||||
"ko": "게시판 목록",
|
||||
"en": "Board List"
|
||||
},
|
||||
"slug": "sirsoft-board-list",
|
||||
"url": "/admin/boards",
|
||||
"icon": "fas fa-list",
|
||||
"order": 2,
|
||||
"is_active": true,
|
||||
"parent_id": 15,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": {
|
||||
"ko": "게시판 신고현황",
|
||||
"en": "Board Reports"
|
||||
},
|
||||
"slug": "sirsoft-board-reports",
|
||||
"url": "/admin/boards/reports",
|
||||
"icon": "fas fa-flag",
|
||||
"order": 3,
|
||||
"is_active": true,
|
||||
"parent_id": 15,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board"
|
||||
}
|
||||
]
|
||||
"slug": "admin-settings",
|
||||
"url": "/admin/settings",
|
||||
"icon": "fas fa-cog",
|
||||
"...": "(3개 키 생략, 총 8개)"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"id": 21,
|
||||
"name": {
|
||||
"ko": "이커머스",
|
||||
"en": "Ecommerce"
|
||||
"ko": "알림 발송 이력",
|
||||
"en": "Notification Logs",
|
||||
"ja": "通知発送履歴"
|
||||
},
|
||||
"slug": "sirsoft-ecommerce",
|
||||
"url": null,
|
||||
"icon": "fas fa-shopping-cart",
|
||||
"order": 40,
|
||||
"is_active": true,
|
||||
"children": [
|
||||
{
|
||||
"id": 4,
|
||||
"name": {
|
||||
"ko": "환경설정",
|
||||
"en": "Settings"
|
||||
},
|
||||
"slug": "sirsoft-ecommerce-settings",
|
||||
"url": "/admin/ecommerce/settings",
|
||||
"icon": "fas fa-cog",
|
||||
"order": 1,
|
||||
"is_active": true,
|
||||
"parent_id": 3,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": {
|
||||
"ko": "상품 관리",
|
||||
"en": "Products"
|
||||
},
|
||||
"slug": "sirsoft-ecommerce-products",
|
||||
"url": "/admin/ecommerce/products",
|
||||
"icon": "fas fa-box",
|
||||
"order": 2,
|
||||
"is_active": true,
|
||||
"parent_id": 3,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": {
|
||||
"ko": "카테고리 관리",
|
||||
"en": "Categories"
|
||||
},
|
||||
"slug": "sirsoft-ecommerce-categories",
|
||||
"url": "/admin/ecommerce/categories",
|
||||
"icon": "fas fa-folder",
|
||||
"order": 3,
|
||||
"is_active": true,
|
||||
"parent_id": 3,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": {
|
||||
"ko": "브랜드 관리",
|
||||
"en": "Brands"
|
||||
},
|
||||
"slug": "sirsoft-ecommerce-brands",
|
||||
"url": "/admin/ecommerce/brands",
|
||||
"icon": "fas fa-tag",
|
||||
"order": 4,
|
||||
"is_active": true,
|
||||
"parent_id": 3,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": {
|
||||
"ko": "상품정보제공고시",
|
||||
"en": "Product Notice"
|
||||
},
|
||||
"slug": "sirsoft-ecommerce-product-notices",
|
||||
"url": "/admin/ecommerce/product-notices",
|
||||
"icon": "fas fa-file-alt",
|
||||
"order": 5,
|
||||
"is_active": true,
|
||||
"parent_id": 3,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": {
|
||||
"ko": "공통정보 관리",
|
||||
"en": "Common Info"
|
||||
},
|
||||
"slug": "sirsoft-ecommerce-common-info",
|
||||
"url": "/admin/ecommerce/common-info",
|
||||
"icon": "fas fa-info-circle",
|
||||
"order": 6,
|
||||
"is_active": true,
|
||||
"parent_id": 3,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": {
|
||||
"ko": "주문 관리",
|
||||
"en": "Orders"
|
||||
},
|
||||
"slug": "sirsoft-ecommerce-orders",
|
||||
"url": "/admin/ecommerce/orders",
|
||||
"icon": "fas fa-receipt",
|
||||
"order": 7,
|
||||
"is_active": true,
|
||||
"parent_id": 3,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": {
|
||||
"ko": "쿠폰 관리",
|
||||
"en": "Coupons"
|
||||
},
|
||||
"slug": "sirsoft-ecommerce-promotion-coupons",
|
||||
"url": "/admin/ecommerce/promotion-coupons",
|
||||
"icon": "fas fa-ticket-alt",
|
||||
"order": 8,
|
||||
"is_active": true,
|
||||
"parent_id": 3,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": {
|
||||
"ko": "배송정책",
|
||||
"en": "Shipping Policies"
|
||||
},
|
||||
"slug": "sirsoft-ecommerce-shipping-policies",
|
||||
"url": "/admin/ecommerce/shipping-policies",
|
||||
"icon": "fas fa-truck",
|
||||
"order": 9,
|
||||
"is_active": true,
|
||||
"parent_id": 3,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": {
|
||||
"ko": "리뷰 관리",
|
||||
"en": "Reviews"
|
||||
},
|
||||
"slug": "sirsoft-ecommerce-reviews",
|
||||
"url": "/admin/ecommerce/reviews",
|
||||
"icon": "fas fa-star",
|
||||
"order": 10,
|
||||
"is_active": true,
|
||||
"parent_id": 3,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": {
|
||||
"ko": "마일리지 내역",
|
||||
"en": "Mileage History"
|
||||
},
|
||||
"slug": "sirsoft-ecommerce-mileage-transactions",
|
||||
"url": "/admin/ecommerce/mileage-transactions",
|
||||
"icon": "fas fa-coins",
|
||||
"order": 11,
|
||||
"is_active": true,
|
||||
"parent_id": 3,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce"
|
||||
}
|
||||
]
|
||||
}
|
||||
"slug": "admin-notification-logs",
|
||||
"url": "/admin/notification-logs",
|
||||
"icon": "fas fa-bell",
|
||||
"...": "(3개 키 생략, 총 8개)"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": {
|
||||
"ko": "본인인증 이력",
|
||||
"en": "Identity Logs",
|
||||
"ja": "本人認証履歴"
|
||||
},
|
||||
"slug": "admin-identity-logs",
|
||||
"url": "/admin/identity/logs",
|
||||
"icon": "fas fa-clipboard-check",
|
||||
"...": "(3개 키 생략, 총 8개)"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": {
|
||||
"ko": "활동 로그",
|
||||
"en": "Activity Logs",
|
||||
"ja": "アクティビティログ"
|
||||
},
|
||||
"slug": "admin-activity-logs",
|
||||
"url": "/admin/activity-logs",
|
||||
"icon": "fas fa-history",
|
||||
"...": "(3개 키 생략, 총 8개)"
|
||||
},
|
||||
"... (총 16건 중 5건 표시)"
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -733,7 +472,7 @@ HTTP/1.1 200
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/menus/extension/{type}/1 HTTP/1.1
|
||||
GET /api/admin/menus/extension/{type}/{identifier} HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -787,7 +526,7 @@ _목록 응답: `data.data[]` 배열 항목의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| id | integer | `19` | 기본 키 (내부 식별자) |
|
||||
| name | object | `{"ko":"대시보드","en":"Dashboard","ja":"ダッシュボード"}` | 메뉴 이름 (다국어 JSON) |
|
||||
| slug | string | `admin-dashboard` | 메뉴 슬러그 |
|
||||
| url | string | `/admin/dashboard` | 메뉴 URL |
|
||||
@@ -811,76 +550,32 @@ HTTP/1.1 200
|
||||
{
|
||||
"id": 19,
|
||||
"name": {
|
||||
"ko": "페이지 관리",
|
||||
"en": "Page Management"
|
||||
"ko": "대시보드",
|
||||
"en": "Dashboard",
|
||||
"ja": "ダッシュボード"
|
||||
},
|
||||
"slug": "sirsoft-page",
|
||||
"url": "/admin/pages",
|
||||
"icon": "fas fa-file-alt",
|
||||
"order": 25,
|
||||
"slug": "admin-dashboard",
|
||||
"url": "/admin/dashboard",
|
||||
"icon": "fas fa-tachometer-alt",
|
||||
"order": 1,
|
||||
"is_active": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"id": 20,
|
||||
"name": {
|
||||
"ko": "게시판 관리",
|
||||
"en": "Board Management"
|
||||
"ko": "환경설정",
|
||||
"en": "Settings",
|
||||
"ja": "環境設定"
|
||||
},
|
||||
"slug": "sirsoft-board",
|
||||
"url": null,
|
||||
"icon": "fas fa-clipboard-list",
|
||||
"order": 30,
|
||||
"slug": "admin-settings",
|
||||
"url": "/admin/settings",
|
||||
"icon": "fas fa-cog",
|
||||
"order": 2,
|
||||
"is_active": true,
|
||||
"children": [
|
||||
{
|
||||
"id": 16,
|
||||
"name": {
|
||||
"ko": "환경설정",
|
||||
"en": "Settings"
|
||||
},
|
||||
"slug": "sirsoft-board-settings",
|
||||
"url": "/admin/boards/settings",
|
||||
"icon": "fas fa-cog",
|
||||
"order": 1,
|
||||
"is_active": true,
|
||||
"parent_id": 15,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": {
|
||||
"ko": "게시판 목록",
|
||||
"en": "Board List"
|
||||
},
|
||||
"slug": "sirsoft-board-list",
|
||||
"url": "/admin/boards",
|
||||
"icon": "fas fa-list",
|
||||
"order": 2,
|
||||
"is_active": true,
|
||||
"parent_id": 15,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": {
|
||||
"ko": "게시판 신고현황",
|
||||
"en": "Board Reports"
|
||||
},
|
||||
"slug": "sirsoft-board-reports",
|
||||
"url": "/admin/boards/reports",
|
||||
"icon": "fas fa-flag",
|
||||
"order": 3,
|
||||
"is_active": true,
|
||||
"parent_id": 15,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board"
|
||||
}
|
||||
]
|
||||
"children": []
|
||||
},
|
||||
"... (총 4건 중 2건 표시)"
|
||||
"... (총 17건 중 2건 표시)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -940,7 +635,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -984,7 +679,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1032,23 +727,23 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `33` | 기본 키 (내부 식별자) |
|
||||
| name | object | `{"ko":"API 문서 샘플 메뉴","en":"API Doc Sample Menu"}` | 메뉴 이름 (다국어 JSON) |
|
||||
| slug | string | `apidoc-sample-menu` | 메뉴 슬러그 |
|
||||
| url | string | `/admin/apidoc-sample` | 메뉴 URL |
|
||||
| icon | string | `fas fa-book` | 메뉴 아이콘 |
|
||||
| order | integer | `28` | 메뉴 순서 |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| name | object | `{"ko":"게시판 관리","en":"Board Management"}` | 메뉴 이름 (다국어 JSON) |
|
||||
| slug | string | `sirsoft-board` | 메뉴 슬러그 |
|
||||
| url | null | `null` | 메뉴 URL |
|
||||
| icon | string | `fas fa-clipboard-list` | 메뉴 아이콘 |
|
||||
| order | integer | `30` | 메뉴 순서 |
|
||||
| is_active | boolean | `true` | active 여부 |
|
||||
| parent_id | null | `null` | 상위 메뉴 ID |
|
||||
| extension_type | null | `null` | 확장 소유 타입: core(코어), module(모듈), plugin(플러그인), NULL(사용자 정의) |
|
||||
| extension_identifier | null | `null` | 확장 식별자 (예: core, sirsoft-board, sirsoft-payment) |
|
||||
| extension_type | string | `module` | 확장 소유 타입: core(코어), module(모듈), plugin(플러그인), NULL(사용자 정의) |
|
||||
| extension_identifier | string | `sirsoft-board` | 확장 식별자 (예: core, sirsoft-board, sirsoft-payment) |
|
||||
| parent | null | `null` | 상위 항목 객체 (parent 관계 파생) |
|
||||
| children | array | `[{"id":34,"name":{"ko":"하위 메뉴","en":"Child Menu"},"slug":…` | 하위 항목 배열 (계층 트리 — children 관계 파생) |
|
||||
| creator | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) |
|
||||
| roles | array | `[]` | 이 메뉴 노출이 허용된 역할 목록 (원소 id/name/permission_type — roles 관계 파생, permission_type 은 pivot 의 노출 권한 유형) |
|
||||
| created_at | string | `2026-07-06 19:15:16` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-06 19:15:16` | 최종 수정 일시 |
|
||||
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
|
||||
| children | array | `[{"id":2,"name":{"ko":"환경설정","en":"Settings"},"slug":"sir…` | 하위 항목 배열 (계층 트리 — children 관계 파생) |
|
||||
| creator | null | `null` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) |
|
||||
| roles | array | `[{"id":1,"name":{"ko":"관리자","en":"Administrator","ja":"管理…` | 이 메뉴 노출이 허용된 역할 목록 (원소 id/name/permission_type — roles 관계 파생, permission_type 은 pivot 의 노출 권한 유형) |
|
||||
| created_at | string | `2026-07-30 18:45:11` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-30 18:45:11` | 최종 수정 일시 |
|
||||
| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
|
||||
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -1064,45 +759,83 @@ HTTP/1.1 200
|
||||
"data": {
|
||||
"id": 1,
|
||||
"name": {
|
||||
"ko": "API 문서 샘플 메뉴",
|
||||
"en": "API Doc Sample Menu"
|
||||
"ko": "게시판 관리",
|
||||
"en": "Board Management"
|
||||
},
|
||||
"slug": "apidoc-sample-menu",
|
||||
"url": "/admin/apidoc-sample",
|
||||
"icon": "fas fa-book",
|
||||
"order": 35,
|
||||
"slug": "sirsoft-board",
|
||||
"url": null,
|
||||
"icon": "fas fa-clipboard-list",
|
||||
"order": 30,
|
||||
"is_active": true,
|
||||
"parent_id": null,
|
||||
"extension_type": null,
|
||||
"extension_identifier": null,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board",
|
||||
"parent": null,
|
||||
"children": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": {
|
||||
"ko": "하위 메뉴",
|
||||
"en": "Child Menu"
|
||||
"ko": "환경설정",
|
||||
"en": "Settings"
|
||||
},
|
||||
"slug": "apidoc-sample-menu-child",
|
||||
"url": "/admin/nihil-non-eos-doloribus-occaecati-optio",
|
||||
"icon": "fas fa-chart",
|
||||
"order": 38,
|
||||
"slug": "sirsoft-board-settings",
|
||||
"url": "/admin/boards/settings",
|
||||
"icon": "fas fa-cog",
|
||||
"order": 1,
|
||||
"is_active": true,
|
||||
"parent_id": 1,
|
||||
"extension_type": null,
|
||||
"extension_identifier": null,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board",
|
||||
"roles": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": {
|
||||
"ko": "게시판 목록",
|
||||
"en": "Board List"
|
||||
},
|
||||
"slug": "sirsoft-board-list",
|
||||
"url": "/admin/boards",
|
||||
"icon": "fas fa-list",
|
||||
"order": 2,
|
||||
"is_active": true,
|
||||
"parent_id": 1,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board",
|
||||
"roles": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": {
|
||||
"ko": "게시판 신고현황",
|
||||
"en": "Board Reports"
|
||||
},
|
||||
"slug": "sirsoft-board-reports",
|
||||
"url": "/admin/boards/reports",
|
||||
"icon": "fas fa-flag",
|
||||
"order": 3,
|
||||
"is_active": true,
|
||||
"parent_id": 1,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board",
|
||||
"roles": []
|
||||
}
|
||||
],
|
||||
"creator": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 사용자",
|
||||
"email": "apidoc-sample-user@example.com"
|
||||
},
|
||||
"roles": [],
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 10:41:24",
|
||||
"is_owner": true,
|
||||
"creator": null,
|
||||
"roles": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": {
|
||||
"ko": "관리자",
|
||||
"en": "Administrator",
|
||||
"ja": "管理者"
|
||||
},
|
||||
"permission_type": "read"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-30 18:45:11",
|
||||
"updated_at": "2026-07-30 18:45:11",
|
||||
"is_owner": false,
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
"can_update": true,
|
||||
@@ -1178,7 +911,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1230,18 +963,18 @@ _단건 응답: `data` 객체의 필드._
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| name | object | `{"ko":"API 문서 샘플 메뉴","en":"API Doc Sample Menu"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| slug | string | `apidoc-sample-menu` | URL 친화 식별자 (slug) |
|
||||
| url | string | `/admin/apidoc-sample` | 메뉴 URL |
|
||||
| icon | string | `fas fa-book` | 아이콘 식별자 (아이콘 클래스/이름) |
|
||||
| order | integer | `35` | 메뉴 순서 |
|
||||
| name | object | `{"ko":"게시판 관리","en":"Board Management"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| slug | string | `sirsoft-board` | URL 친화 식별자 (slug) |
|
||||
| url | null | `null` | 메뉴 URL |
|
||||
| icon | string | `fas fa-clipboard-list` | 아이콘 식별자 (아이콘 클래스/이름) |
|
||||
| order | integer | `30` | 메뉴 순서 |
|
||||
| is_active | boolean | `false` | active 여부 |
|
||||
| parent_id | null | `null` | parent 식별자 (연관 리소스 참조) |
|
||||
| extension_type | null | `null` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) |
|
||||
| extension_identifier | null | `null` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| created_at | string | `2026-07-08 10:41:24` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 12:14:40` | 최종 수정 일시 |
|
||||
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
|
||||
| extension_type | string | `module` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) |
|
||||
| extension_identifier | string | `sirsoft-board` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| created_at | string | `2026-07-30 18:45:11` | 생성 일시 |
|
||||
| updated_at | string | `2026-08-04 21:53:41` | 최종 수정 일시 |
|
||||
| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
|
||||
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -1257,20 +990,20 @@ HTTP/1.1 200
|
||||
"data": {
|
||||
"id": 1,
|
||||
"name": {
|
||||
"ko": "API 문서 샘플 메뉴",
|
||||
"en": "API Doc Sample Menu"
|
||||
"ko": "게시판 관리",
|
||||
"en": "Board Management"
|
||||
},
|
||||
"slug": "apidoc-sample-menu",
|
||||
"url": "/admin/apidoc-sample",
|
||||
"icon": "fas fa-book",
|
||||
"order": 35,
|
||||
"slug": "sirsoft-board",
|
||||
"url": null,
|
||||
"icon": "fas fa-clipboard-list",
|
||||
"order": 30,
|
||||
"is_active": false,
|
||||
"parent_id": null,
|
||||
"extension_type": null,
|
||||
"extension_identifier": null,
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 12:14:40",
|
||||
"is_owner": true,
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board",
|
||||
"created_at": "2026-07-30 18:45:11",
|
||||
"updated_at": "2026-08-04 21:53:41",
|
||||
"is_owner": false,
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
"can_update": true,
|
||||
|
||||
+270
-10867
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Notification Channels 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Notification Definitions 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -31,6 +31,7 @@
|
||||
| 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 등) |
|
||||
| template_channel | query | string | 아니오 | max 50 | 목록에 실을 템플릿을 한 채널로 좁힙니다. `channel` 과 달리 정의 행은 그대로 두고 각 행의 `templates` 만 좁힙니다. **지정하지 않으면 `templates` 키 자체가 응답에 없습니다** — 정의 하나당 채널 × 로케일만큼의 제목·본문이 실리는 것을 막기 위한 기본값이며, 전 채널이 필요하면 단건 조회를 이용하세요 |
|
||||
| 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` | 정렬 기준 필드명 |
|
||||
@@ -41,7 +42,7 @@
|
||||
**요청 예시**
|
||||
|
||||
```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
|
||||
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&template_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}
|
||||
@@ -55,20 +56,21 @@ _목록 응답: `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…` | 다국어 설명 |
|
||||
| type | string | `new_comment` | 알림 타입 (welcome, order_confirmed 등) |
|
||||
| hook_prefix | string | `sirsoft-board` | 훅 접두사 (core.auth, sirsoft-ecommerce 등) |
|
||||
| extension_type | string | `module` | 확장 타입: core, module, plugin |
|
||||
| extension_identifier | string | `sirsoft-board` | 확장 식별자: core, sirsoft-board 등 |
|
||||
| name | object | `{"ko":"새 댓글 알림","en":"New Comment Notification"}` | 다국어 이름 ({"ko": "회원가입 환영", "en": "Welcome"}) |
|
||||
| description | object | `{"ko":"게시글에 새 댓글이 작성되면 게시글 작성자에게 발송","en":"Sent to post a…` | 다국어 설명 |
|
||||
| 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"]) |
|
||||
| hooks | array | `["sirsoft-board.comment.after_create"]` | 트리거 훅 목록 (["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` | 최종 수정 일시 |
|
||||
| has_customized_templates | boolean | `false` | 기본값에서 수정된 템플릿이 하나라도 있는지. `template_channel` 로 좁혀도 **모든 채널**을 기준으로 판정하므로, 지금 보고 있지 않은 채널만 수정된 경우에도 true 다 (되돌리기 버튼 노출 조건) |
|
||||
| created_at | string | `2026-07-30 18:45:11` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-30 18:45:11` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -86,199 +88,28 @@ HTTP/1.1 200
|
||||
{
|
||||
"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
|
||||
}
|
||||
"type": "new_comment",
|
||||
"hook_prefix": "sirsoft-board",
|
||||
"extension_type": "module",
|
||||
"...": "(13개 키 생략, 총 18개)"
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"id": 2,
|
||||
"type": "order_confirmed",
|
||||
"hook_prefix": "sirsoft-ecommerce",
|
||||
"type": "reply_comment",
|
||||
"hook_prefix": "sirsoft-board",
|
||||
"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
|
||||
}
|
||||
"...": "(13개 키 생략, 총 18개)"
|
||||
},
|
||||
"... (총 18건 중 2건 표시)"
|
||||
"... (총 23건 중 2건 표시)"
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"per_page": 25,
|
||||
"total": 18,
|
||||
"total": 23,
|
||||
"from": 1,
|
||||
"to": 18,
|
||||
"has_more_pages": false
|
||||
"...": "(2개 키 생략, 총 7개)"
|
||||
},
|
||||
"abilities": {
|
||||
"can_update": true
|
||||
@@ -327,21 +158,21 @@ _단건 응답: `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"]) |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| type | string | `new_comment` | 알림 타입 (welcome, order_confirmed 등) |
|
||||
| hook_prefix | string | `sirsoft-board` | 훅 접두사 (core.auth, sirsoft-ecommerce 등) |
|
||||
| extension_type | string | `module` | 확장 타입: core, module, plugin |
|
||||
| extension_identifier | string | `sirsoft-board` | 확장 식별자: core, sirsoft-board 등 |
|
||||
| name | object | `{"ko":"새 댓글 알림","en":"New Comment Notification"}` | 다국어 이름 ({"ko": "회원가입 환영", "en": "Welcome"}) |
|
||||
| description | object | `{"ko":"게시글에 새 댓글이 작성되면 게시글 작성자에게 발송","en":"Sent to post a…` | 다국어 설명 |
|
||||
| variables | array | `[{"key":"name","description":"수신자 이름"},{"key":"app_name",…` | 사용 가능 변수 메타데이터 ([{key, description}]) |
|
||||
| channels | array | `["mail","database"]` | 활성 채널 (["mail", "database"]) |
|
||||
| hooks | array | `["sirsoft-board.comment.after_create"]` | 트리거 훅 목록 (["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` | 최종 수정 일시 |
|
||||
| is_default | boolean | `true` | default 여부 |
|
||||
| templates | array | `[{"id":2,"definition_id":1,"channel":"database","subject"…` | 채널별 알림 템플릿 목록 (templates 관계 로드 시 NotificationTemplateResource 배열, 미로드 시 null) |
|
||||
| created_at | string | `2026-07-30 18:45:11` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-30 18:45:11` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -356,55 +187,127 @@ HTTP/1.1 200
|
||||
"message": "알림 정의를 조회했습니다.",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"type": "apidoc-sample.event",
|
||||
"hook_prefix": "core",
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "",
|
||||
"type": "new_comment",
|
||||
"hook_prefix": "sirsoft-board",
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board",
|
||||
"name": {
|
||||
"ko": "API 문서 샘플 알림",
|
||||
"en": "API Doc Sample Notification"
|
||||
"ko": "새 댓글 알림",
|
||||
"en": "New Comment Notification"
|
||||
},
|
||||
"description": {
|
||||
"ko": "문서 실측용 알림 정의",
|
||||
"en": "Sample notification"
|
||||
"ko": "게시글에 새 댓글이 작성되면 게시글 작성자에게 발송",
|
||||
"en": "Sent to post author when a new comment is posted"
|
||||
},
|
||||
"variables": [],
|
||||
"variables": [
|
||||
{
|
||||
"key": "name",
|
||||
"description": "수신자 이름"
|
||||
},
|
||||
{
|
||||
"key": "app_name",
|
||||
"description": "사이트명"
|
||||
},
|
||||
{
|
||||
"key": "board_name",
|
||||
"description": "게시판 이름"
|
||||
},
|
||||
{
|
||||
"key": "post_title",
|
||||
"description": "게시글 제목"
|
||||
},
|
||||
{
|
||||
"key": "comment_author",
|
||||
"description": "댓글 작성자"
|
||||
},
|
||||
{
|
||||
"key": "comment_content",
|
||||
"description": "댓글 내용 (200자)"
|
||||
},
|
||||
{
|
||||
"key": "post_url",
|
||||
"description": "게시글 URL"
|
||||
},
|
||||
{
|
||||
"key": "site_url",
|
||||
"description": "사이트 URL"
|
||||
}
|
||||
],
|
||||
"channels": [
|
||||
"database",
|
||||
"mail"
|
||||
"mail",
|
||||
"database"
|
||||
],
|
||||
"hooks": [
|
||||
"sirsoft-board.comment.after_create"
|
||||
],
|
||||
"hooks": [],
|
||||
"is_active": true,
|
||||
"is_default": false,
|
||||
"is_default": true,
|
||||
"templates": [
|
||||
{
|
||||
"id": 2,
|
||||
"definition_id": 1,
|
||||
"channel": "database",
|
||||
"subject": {
|
||||
"ko": "게시글에 새 댓글이 달렸습니다",
|
||||
"en": "New comment on your post"
|
||||
},
|
||||
"body": {
|
||||
"ko": "{comment_author}님이 '{board_name}' 게시글 '{post_title}'에 댓글을 남겼습니다.",
|
||||
"en": "{comment_author} commented on your post '{post_title}' in '{board_name}'."
|
||||
},
|
||||
"click_url": "{post_url}",
|
||||
"recipients": [
|
||||
{
|
||||
"type": "related_user",
|
||||
"relation": "post_author",
|
||||
"exclude_trigger_user": true
|
||||
}
|
||||
],
|
||||
"is_active": true,
|
||||
"is_default": true,
|
||||
"user_overrides": null,
|
||||
"updated_by": null,
|
||||
"created_at": "2026-07-30 18:45:11",
|
||||
"updated_at": "2026-07-30 18:45:11",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"definition_id": 1,
|
||||
"channel": "mail",
|
||||
"subject": "API 문서 샘플 템플릿 제목",
|
||||
"body": "안녕하세요 {{name}} 님, 문서 실측용 본문입니다.",
|
||||
"click_url": "/admin/apidoc-sample",
|
||||
"subject": {
|
||||
"ko": "[{board_name}] 게시글에 새 댓글이 등록되었습니다",
|
||||
"en": "[{board_name}] New comment on your post"
|
||||
},
|
||||
"body": {
|
||||
"ko": "<h1>{name}님, 안녕하세요.</h1><p><strong>{board_name}</strong> 게시판의 게시글에 <strong>{comment_author}</strong>님이 댓글을 남겼습니다.</p><blockquote style=\"border-left: 3px solid #cbd5e0; padding-left: 12px; color: #718096;\">{comment_content}</blockquote><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{post_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">게시글 보기</a></td></tr></table><p>감사합니다,<br><a href=\"{site_url}\">{app_name}</a></p>",
|
||||
"en": "<h1>Hello, {name}.</h1><p><strong>{comment_author}</strong> commented on your post in <strong>{board_name}</strong>.</p><blockquote style=\"border-left: 3px solid #cbd5e0; padding-left: 12px; color: #718096;\">{comment_content}</blockquote><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{post_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">View Post</a></td></tr></table><p>Thank you,<br><a href=\"{site_url}\">{app_name}</a></p>"
|
||||
},
|
||||
"click_url": null,
|
||||
"recipients": [
|
||||
{
|
||||
"type": "role",
|
||||
"value": "admin",
|
||||
"display_name": "관리자"
|
||||
"type": "related_user",
|
||||
"relation": "post_author",
|
||||
"exclude_trigger_user": true
|
||||
}
|
||||
],
|
||||
"is_active": true,
|
||||
"is_default": false,
|
||||
"is_default": true,
|
||||
"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",
|
||||
"updated_by": null,
|
||||
"created_at": "2026-07-30 18:45:11",
|
||||
"updated_at": "2026-07-30 18:45:11",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 10:41:24",
|
||||
"created_at": "2026-07-30 18:45:11",
|
||||
"updated_at": "2026-07-30 18:45:11",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
@@ -465,7 +368,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -513,20 +416,20 @@ _단건 응답: `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"]) |
|
||||
| type | string | `new_comment` | 알림 타입 (welcome, order_confirmed 등) |
|
||||
| hook_prefix | string | `sirsoft-board` | 훅 접두사 (core.auth, sirsoft-ecommerce 등) |
|
||||
| extension_type | string | `module` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) |
|
||||
| extension_identifier | string | `sirsoft-board` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| name | object | `{"ko":"새 댓글 알림","en":"New Comment Notification"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| description | object | `{"ko":"게시글에 새 댓글이 작성되면 게시글 작성자에게 발송","en":"Sent to post a…` | 설명 (다국어 필드는 로케일별 값 객체) |
|
||||
| variables | array | `[{"key":"name","description":"수신자 이름"},{"key":"app_name",…` | 사용 가능 변수 메타데이터 ([{key, description}]) |
|
||||
| channels | array | `["mail","database"]` | 활성 채널 (["mail", "database"]) |
|
||||
| hooks | array | `["sirsoft-board.comment.after_create"]` | 트리거 훅 목록 (["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` | 최종 수정 일시 |
|
||||
| templates | array | `[{"id":2,"definition_id":1,"channel":"database","subject"…` | 템플릿 목록 (각 원소 identifier/name 등 — 템플릿 관계 파생) |
|
||||
| created_at | string | `2026-07-30 18:45:11` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-30 18:45:11` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -541,55 +444,127 @@ HTTP/1.1 200
|
||||
"message": "알림 정의의 모든 템플릿이 기본값으로 복원되었습니다.",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"type": "apidoc-sample.event",
|
||||
"hook_prefix": "core",
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "",
|
||||
"type": "new_comment",
|
||||
"hook_prefix": "sirsoft-board",
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board",
|
||||
"name": {
|
||||
"ko": "API 문서 샘플 알림",
|
||||
"en": "API Doc Sample Notification"
|
||||
"ko": "새 댓글 알림",
|
||||
"en": "New Comment Notification"
|
||||
},
|
||||
"description": {
|
||||
"ko": "문서 실측용 알림 정의",
|
||||
"en": "Sample notification"
|
||||
"ko": "게시글에 새 댓글이 작성되면 게시글 작성자에게 발송",
|
||||
"en": "Sent to post author when a new comment is posted"
|
||||
},
|
||||
"variables": [],
|
||||
"variables": [
|
||||
{
|
||||
"key": "name",
|
||||
"description": "수신자 이름"
|
||||
},
|
||||
{
|
||||
"key": "app_name",
|
||||
"description": "사이트명"
|
||||
},
|
||||
{
|
||||
"key": "board_name",
|
||||
"description": "게시판 이름"
|
||||
},
|
||||
{
|
||||
"key": "post_title",
|
||||
"description": "게시글 제목"
|
||||
},
|
||||
{
|
||||
"key": "comment_author",
|
||||
"description": "댓글 작성자"
|
||||
},
|
||||
{
|
||||
"key": "comment_content",
|
||||
"description": "댓글 내용 (200자)"
|
||||
},
|
||||
{
|
||||
"key": "post_url",
|
||||
"description": "게시글 URL"
|
||||
},
|
||||
{
|
||||
"key": "site_url",
|
||||
"description": "사이트 URL"
|
||||
}
|
||||
],
|
||||
"channels": [
|
||||
"database",
|
||||
"mail"
|
||||
"mail",
|
||||
"database"
|
||||
],
|
||||
"hooks": [
|
||||
"sirsoft-board.comment.after_create"
|
||||
],
|
||||
"hooks": [],
|
||||
"is_active": true,
|
||||
"is_default": true,
|
||||
"templates": [
|
||||
{
|
||||
"id": 1,
|
||||
"id": 2,
|
||||
"definition_id": 1,
|
||||
"channel": "mail",
|
||||
"subject": "API 문서 샘플 템플릿 제목",
|
||||
"body": "안녕하세요 {{name}} 님, 문서 실측용 본문입니다.",
|
||||
"click_url": "/admin/apidoc-sample",
|
||||
"channel": "database",
|
||||
"subject": {
|
||||
"ko": "게시글에 새 댓글이 달렸습니다",
|
||||
"en": "New comment on your post"
|
||||
},
|
||||
"body": {
|
||||
"ko": "{comment_author}님이 '{board_name}' 게시글 '{post_title}'에 댓글을 남겼습니다.",
|
||||
"en": "{comment_author} commented on your post '{post_title}' in '{board_name}'."
|
||||
},
|
||||
"click_url": "{post_url}",
|
||||
"recipients": [
|
||||
{
|
||||
"type": "role",
|
||||
"value": "admin",
|
||||
"display_name": "관리자"
|
||||
"type": "related_user",
|
||||
"relation": "post_author",
|
||||
"exclude_trigger_user": true
|
||||
}
|
||||
],
|
||||
"is_active": true,
|
||||
"is_default": false,
|
||||
"is_default": true,
|
||||
"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",
|
||||
"updated_by": null,
|
||||
"created_at": "2026-07-30 18:45:11",
|
||||
"updated_at": "2026-07-30 18:45:11",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"definition_id": 1,
|
||||
"channel": "mail",
|
||||
"subject": {
|
||||
"ko": "[{board_name}] 게시글에 새 댓글이 등록되었습니다",
|
||||
"en": "[{board_name}] New comment on your post"
|
||||
},
|
||||
"body": {
|
||||
"ko": "<h1>{name}님, 안녕하세요.</h1><p><strong>{board_name}</strong> 게시판의 게시글에 <strong>{comment_author}</strong>님이 댓글을 남겼습니다.</p><blockquote style=\"border-left: 3px solid #cbd5e0; padding-left: 12px; color: #718096;\">{comment_content}</blockquote><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{post_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">게시글 보기</a></td></tr></table><p>감사합니다,<br><a href=\"{site_url}\">{app_name}</a></p>",
|
||||
"en": "<h1>Hello, {name}.</h1><p><strong>{comment_author}</strong> commented on your post in <strong>{board_name}</strong>.</p><blockquote style=\"border-left: 3px solid #cbd5e0; padding-left: 12px; color: #718096;\">{comment_content}</blockquote><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{post_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">View Post</a></td></tr></table><p>Thank you,<br><a href=\"{site_url}\">{app_name}</a></p>"
|
||||
},
|
||||
"click_url": null,
|
||||
"recipients": [
|
||||
{
|
||||
"type": "related_user",
|
||||
"relation": "post_author",
|
||||
"exclude_trigger_user": true
|
||||
}
|
||||
],
|
||||
"is_active": true,
|
||||
"is_default": true,
|
||||
"user_overrides": null,
|
||||
"updated_by": null,
|
||||
"created_at": "2026-07-30 18:45:11",
|
||||
"updated_at": "2026-07-30 18:45:11",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 12:14:43",
|
||||
"created_at": "2026-07-30 18:45:11",
|
||||
"updated_at": "2026-07-30 18:45:11",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
@@ -639,20 +614,19 @@ _단건 응답: `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"]) |
|
||||
| type | string | `new_comment` | 알림 타입 (welcome, order_confirmed 등) |
|
||||
| hook_prefix | string | `sirsoft-board` | 훅 접두사 (core.auth, sirsoft-ecommerce 등) |
|
||||
| extension_type | string | `module` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) |
|
||||
| extension_identifier | string | `sirsoft-board` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| name | object | `{"ko":"새 댓글 알림","en":"New Comment Notification"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| description | object | `{"ko":"게시글에 새 댓글이 작성되면 게시글 작성자에게 발송","en":"Sent to post a…` | 설명 (다국어 필드는 로케일별 값 객체) |
|
||||
| variables | array | `[{"key":"name","description":"수신자 이름"},{"key":"app_name",…` | 사용 가능 변수 메타데이터 ([{key, description}]) |
|
||||
| channels | array | `["mail","database"]` | 활성 채널 (["mail", "database"]) |
|
||||
| hooks | array | `["sirsoft-board.comment.after_create"]` | 트리거 훅 목록 (["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` | 최종 수정 일시 |
|
||||
| is_default | boolean | `true` | default 여부 |
|
||||
| created_at | string | `2026-07-30 18:45:11` | 생성 일시 |
|
||||
| updated_at | string | `2026-08-04 21:53:43` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -667,29 +641,63 @@ HTTP/1.1 200
|
||||
"message": "알림 정의 활성 상태가 변경되었습니다.",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"type": "apidoc-sample.event",
|
||||
"hook_prefix": "core",
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "",
|
||||
"type": "new_comment",
|
||||
"hook_prefix": "sirsoft-board",
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-board",
|
||||
"name": {
|
||||
"ko": "API 문서 샘플 알림",
|
||||
"en": "API Doc Sample Notification"
|
||||
"ko": "새 댓글 알림",
|
||||
"en": "New Comment Notification"
|
||||
},
|
||||
"description": {
|
||||
"ko": "문서 실측용 알림 정의",
|
||||
"en": "Sample notification"
|
||||
"ko": "게시글에 새 댓글이 작성되면 게시글 작성자에게 발송",
|
||||
"en": "Sent to post author when a new comment is posted"
|
||||
},
|
||||
"variables": [],
|
||||
"variables": [
|
||||
{
|
||||
"key": "name",
|
||||
"description": "수신자 이름"
|
||||
},
|
||||
{
|
||||
"key": "app_name",
|
||||
"description": "사이트명"
|
||||
},
|
||||
{
|
||||
"key": "board_name",
|
||||
"description": "게시판 이름"
|
||||
},
|
||||
{
|
||||
"key": "post_title",
|
||||
"description": "게시글 제목"
|
||||
},
|
||||
{
|
||||
"key": "comment_author",
|
||||
"description": "댓글 작성자"
|
||||
},
|
||||
{
|
||||
"key": "comment_content",
|
||||
"description": "댓글 내용 (200자)"
|
||||
},
|
||||
{
|
||||
"key": "post_url",
|
||||
"description": "게시글 URL"
|
||||
},
|
||||
{
|
||||
"key": "site_url",
|
||||
"description": "사이트 URL"
|
||||
}
|
||||
],
|
||||
"channels": [
|
||||
"database",
|
||||
"mail"
|
||||
"mail",
|
||||
"database"
|
||||
],
|
||||
"hooks": [
|
||||
"sirsoft-board.comment.after_create"
|
||||
],
|
||||
"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",
|
||||
"is_default": true,
|
||||
"created_at": "2026-07-30 18:45:11",
|
||||
"updated_at": "2026-08-04 21:53:43",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Notification Logs 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -55,26 +55,26 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| number | integer | `677` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) |
|
||||
| id | integer | `677` | 기본 키 (내부 식별자) |
|
||||
| number | integer | `7549` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) |
|
||||
| id | integer | `7559` | 기본 키 (내부 식별자) |
|
||||
| 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_user_id | integer | `1240` | 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 관계 파생) |
|
||||
| sender_user_id | integer | `1240` | sender user 식별자 (연관 리소스 참조) |
|
||||
| sender | object | `{"uuid":"a2640dae-e87c-4a28-b4f1-481fd961dc02","name":"AP…` | 발신자 사용자 객체 (uuid/name/email — senderUser 관계 파생) |
|
||||
| recipient | object | `{"uuid":"a2640dae-e87c-4a28-b4f1-481fd961dc02","name":"AP…` | 수신자 사용자 객체 (uuid/name/email — recipientUser 관계 파생) |
|
||||
| subject | string | `API 문서 샘플 알림` | 렌더링된 제목 |
|
||||
| body | string | `문서 실측용 알림 본문입니다.` | 렌더링된 본문 |
|
||||
| status | string | `sent` | 상태: sent, failed, skipped |
|
||||
| error_message | string | `해당 채널은 비회원 발송을 허용하지 않아 발송을 건너뛰었습니다.` | 에러 메시지 |
|
||||
| error_message | string | `SMTP 호스트가 설정되지 않았습니다.` | 에러 메시지 |
|
||||
| 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` | 최종 수정 일시 |
|
||||
| sent_at | string | `2026-07-31 21:55:00` | sent 일시 |
|
||||
| created_at | string | `2026-07-31 22:55:00` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-31 22:55:00` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -90,23 +90,23 @@ HTTP/1.1 200
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"number": 1,
|
||||
"id": 1,
|
||||
"number": 7549,
|
||||
"id": 7559,
|
||||
"channel": "mail",
|
||||
"notification_type": "apidoc.sample.event",
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "",
|
||||
"recipient_user_id": 6,
|
||||
"recipient_user_id": 1240,
|
||||
"recipient_identifier": "apidoc-sample-user@example.com",
|
||||
"recipient_name": "API 문서 샘플 사용자",
|
||||
"sender_user_id": 6,
|
||||
"sender_user_id": 1240,
|
||||
"sender": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"uuid": "a2640dae-e87c-4a28-b4f1-481fd961dc02",
|
||||
"name": "API 문서 샘플 사용자",
|
||||
"email": "apidoc-sample-user@example.com"
|
||||
},
|
||||
"recipient": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"uuid": "a2640dae-e87c-4a28-b4f1-481fd961dc02",
|
||||
"name": "API 문서 샘플 사용자",
|
||||
"email": "apidoc-sample-user@example.com"
|
||||
},
|
||||
@@ -115,22 +115,48 @@ HTTP/1.1 200
|
||||
"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",
|
||||
"sent_at": "2026-07-31 21:55:00",
|
||||
"created_at": "2026-07-31 22:55:00",
|
||||
"updated_at": "2026-07-31 22:55:00",
|
||||
"abilities": {
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"number": 7548,
|
||||
"id": 7558,
|
||||
"channel": "mail",
|
||||
"notification_type": "inquiry_received",
|
||||
"extension_type": "module",
|
||||
"extension_identifier": "sirsoft-ecommerce",
|
||||
"recipient_user_id": null,
|
||||
"recipient_identifier": "saemi.cho@example.com",
|
||||
"recipient_name": "송민성",
|
||||
"sender_user_id": null,
|
||||
"sender": null,
|
||||
"recipient": null,
|
||||
"subject": null,
|
||||
"body": null,
|
||||
"status": "skipped",
|
||||
"error_message": "SMTP 호스트가 설정되지 않았습니다.",
|
||||
"source": "notification",
|
||||
"sent_at": "2026-07-31 21:41:59",
|
||||
"created_at": "2026-07-31 21:41:59",
|
||||
"updated_at": "2026-07-31 21:41:59",
|
||||
"abilities": {
|
||||
"can_delete": true
|
||||
}
|
||||
},
|
||||
"... (총 25건 중 2건 표시)"
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"last_page": 302,
|
||||
"per_page": 25,
|
||||
"total": 1,
|
||||
"total": 7549,
|
||||
"from": 1,
|
||||
"to": 1,
|
||||
"has_more_pages": false
|
||||
"to": 25,
|
||||
"has_more_pages": true
|
||||
},
|
||||
"abilities": {
|
||||
"can_delete": true
|
||||
@@ -182,7 +208,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Notification Templates 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -28,7 +28,7 @@
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| definition_id | body | integer | 예 | — | definition 식별자 |
|
||||
| subject | body | array | 아니오 | — | 다국어 제목. 컬럼이 nullable 이므로 미전송·`null` 이 허용된다(제목 개념이 없는 채널 대응). 전송하는 경우 로케일별 문자열 배열이어야 하며 각 값은 500자 이하 |
|
||||
| subject | body | array | 예 | — | 다국어 제목. 컬럼이 nullable 이므로 미전송·`null` 이 허용된다(제목 개념이 없는 채널 대응). 전송하는 경우 로케일별 문자열 배열이어야 하며 각 값은 500자 이하 |
|
||||
| body | body | array | 예 | — | 본문 |
|
||||
| locale | body | string | 아니오 | max 10 | 로케일 코드 (표시 언어/지역) |
|
||||
|
||||
@@ -55,7 +55,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -119,7 +119,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -162,11 +162,68 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `1` | 기본 키 (내부 식별자) |
|
||||
| definition_id | integer | `1` | definition 식별자 (연관 리소스 참조) |
|
||||
| channel | string | `mail` | 채널: mail, database, fcm |
|
||||
| subject | object | `{"ko":"[{board_name}] 게시글에 새 댓글이 등록되었습니다","en":"[{board_n…` | 다국어 제목 ({"ko": "...", "en": "..."}) |
|
||||
| body | object | `{"ko":"<h1>{name}님, 안녕하세요.<\/h1><p><strong>{board_name}<\…` | 다국어 본문 ({"ko": "...", "en": "..."}) |
|
||||
| click_url | null | `null` | click URL |
|
||||
| recipients | array | `[{"type":"related_user","relation":"post_author","exclude…` | 수신자 규칙 JSON ([{type, value, relation, exclude_trigger_user}]) |
|
||||
| is_active | boolean | `true` | active 여부 |
|
||||
| is_default | boolean | `true` | default 여부 |
|
||||
| user_overrides | null | `null` | 사용자가 수정한 필드명 목록 |
|
||||
| updated_by | null | `null` | 최종 수정한 사용자 정보 (uuid/name — updated_by 관계 파생, 없으면 null) |
|
||||
| created_at | string | `2026-07-30 18:45:11` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-30 18:45:11` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. -->
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "알림 템플릿이 기본값으로 복원되었습니다.",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"definition_id": 1,
|
||||
"channel": "mail",
|
||||
"subject": {
|
||||
"ko": "[{board_name}] 게시글에 새 댓글이 등록되었습니다",
|
||||
"en": "[{board_name}] New comment on your post"
|
||||
},
|
||||
"body": {
|
||||
"ko": "<h1>{name}님, 안녕하세요.</h1><p><strong>{board_name}</strong> 게시판의 게시글에 <strong>{comment_author}</strong>님이 댓글을 남겼습니다.</p><blockquote style=\"border-left: 3px solid #cbd5e0; padding-left: 12px; color: #718096;\">{comment_content}</blockquote><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{post_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">게시글 보기</a></td></tr></table><p>감사합니다,<br><a href=\"{site_url}\">{app_name}</a></p>",
|
||||
"en": "<h1>Hello, {name}.</h1><p><strong>{comment_author}</strong> commented on your post in <strong>{board_name}</strong>.</p><blockquote style=\"border-left: 3px solid #cbd5e0; padding-left: 12px; color: #718096;\">{comment_content}</blockquote><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{post_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">View Post</a></td></tr></table><p>Thank you,<br><a href=\"{site_url}\">{app_name}</a></p>"
|
||||
},
|
||||
"click_url": null,
|
||||
"recipients": [
|
||||
{
|
||||
"type": "related_user",
|
||||
"relation": "post_author",
|
||||
"exclude_trigger_user": true
|
||||
}
|
||||
],
|
||||
"is_active": true,
|
||||
"is_default": true,
|
||||
"user_overrides": null,
|
||||
"updated_by": null,
|
||||
"created_at": "2026-07-30 18:45:11",
|
||||
"updated_at": "2026-07-30 18:45:11",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -211,16 +268,16 @@ _단건 응답: `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}]) |
|
||||
| subject | object | `{"ko":"[{board_name}] 게시글에 새 댓글이 등록되었습니다","en":"[{board_n…` | 다국어 제목 ({"ko": "...", "en": "..."}) |
|
||||
| body | object | `{"ko":"<h1>{name}님, 안녕하세요.<\/h1><p><strong>{board_name}<\…` | 다국어 본문 ({"ko": "...", "en": "..."}) |
|
||||
| click_url | null | `null` | click URL |
|
||||
| recipients | array | `[{"type":"related_user","relation":"post_author","exclude…` | 수신자 규칙 JSON ([{type, value, relation, exclude_trigger_user}]) |
|
||||
| is_active | boolean | `false` | active 여부 |
|
||||
| is_default | boolean | `false` | default 여부 |
|
||||
| is_default | boolean | `true` | 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` | 최종 수정 일시 |
|
||||
| updated_by | null | `null` | 최종 수정한 사용자 정보 (uuid/name — updated_by 관계 파생, 없으면 null) |
|
||||
| created_at | string | `2026-07-30 18:45:11` | 생성 일시 |
|
||||
| updated_at | string | `2026-08-04 21:53:43` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -237,24 +294,30 @@ HTTP/1.1 200
|
||||
"id": 1,
|
||||
"definition_id": 1,
|
||||
"channel": "mail",
|
||||
"subject": "API 문서 샘플 템플릿 제목",
|
||||
"body": "안녕하세요 {{name}} 님, 문서 실측용 본문입니다.",
|
||||
"click_url": "/admin/apidoc-sample",
|
||||
"subject": {
|
||||
"ko": "[{board_name}] 게시글에 새 댓글이 등록되었습니다",
|
||||
"en": "[{board_name}] New comment on your post"
|
||||
},
|
||||
"body": {
|
||||
"ko": "<h1>{name}님, 안녕하세요.</h1><p><strong>{board_name}</strong> 게시판의 게시글에 <strong>{comment_author}</strong>님이 댓글을 남겼습니다.</p><blockquote style=\"border-left: 3px solid #cbd5e0; padding-left: 12px; color: #718096;\">{comment_content}</blockquote><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{post_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">게시글 보기</a></td></tr></table><p>감사합니다,<br><a href=\"{site_url}\">{app_name}</a></p>",
|
||||
"en": "<h1>Hello, {name}.</h1><p><strong>{comment_author}</strong> commented on your post in <strong>{board_name}</strong>.</p><blockquote style=\"border-left: 3px solid #cbd5e0; padding-left: 12px; color: #718096;\">{comment_content}</blockquote><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 24px 0;\"><tr><td align=\"center\"><a href=\"{post_url}\" style=\"display: inline-block; padding: 12px 32px; background-color: #2d3748; color: #ffffff; text-decoration: none; border-radius: 4px; font-weight: 600; font-size: 14px;\">View Post</a></td></tr></table><p>Thank you,<br><a href=\"{site_url}\">{app_name}</a></p>"
|
||||
},
|
||||
"click_url": null,
|
||||
"recipients": [
|
||||
{
|
||||
"type": "role",
|
||||
"value": "admin",
|
||||
"display_name": "관리자"
|
||||
"type": "related_user",
|
||||
"relation": "post_author",
|
||||
"exclude_trigger_user": true
|
||||
}
|
||||
],
|
||||
"is_active": false,
|
||||
"is_default": false,
|
||||
"is_default": true,
|
||||
"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",
|
||||
"updated_by": null,
|
||||
"created_at": "2026-07-30 18:45:11",
|
||||
"updated_at": "2026-08-04 21:53:43",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Notifications 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -47,7 +47,18 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
|
||||
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| number | integer | `23` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) |
|
||||
| id | string | `518b9c94-5853-4b68-8193-83af1851bbc6` | 기본 키 (내부 식별자) |
|
||||
| type | string | `inquiry_received` | <!-- TODO: 설명 --> |
|
||||
| type_label | string | `상품 문의 접수` | `type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) |
|
||||
| subject | string | `새로운 상품 문의가 접수되었습니다` | <!-- TODO: 설명 --> |
|
||||
| body | string | `옥혜진님이 "면 손수건 3매입 #1" 상품에 문의를 남겼습니다.` | <!-- TODO: 설명 --> |
|
||||
| url | string | `https://g7_2.dev/admin/ecommerce/prod…` | <!-- TODO: 설명 --> |
|
||||
| data | object | `{"type":"inquiry_received","subject":"새로운 상품 문의가 접수되었습니다"…` | <!-- TODO: 설명 --> |
|
||||
| read_at | string | `2026-07-31 22:06:53` | read 일시 |
|
||||
| created_at | string | `2026-07-31 21:41:58` | 생성 일시 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -60,14 +71,68 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "알림 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"data": [],
|
||||
"data": [
|
||||
{
|
||||
"number": 23,
|
||||
"id": "518b9c94-5853-4b68-8193-83af1851bbc6",
|
||||
"type": "inquiry_received",
|
||||
"type_label": "상품 문의 접수",
|
||||
"subject": "새로운 상품 문의가 접수되었습니다",
|
||||
"body": "옥혜진님이 \"면 손수건 3매입 #1\" 상품에 문의를 남겼습니다.",
|
||||
"url": "https://api.example.com/admin/ecommerce/product-inquiries",
|
||||
"data": {
|
||||
"type": "inquiry_received",
|
||||
"subject": "새로운 상품 문의가 접수되었습니다",
|
||||
"body": "옥혜진님이 \"면 손수건 3매입 #1\" 상품에 문의를 남겼습니다.",
|
||||
"click_url": "https://api.example.com/admin/ecommerce/product-inquiries",
|
||||
"data": {
|
||||
"name": "최고관리자",
|
||||
"app_name": "그누보드7",
|
||||
"product_name": "면 손수건 3매입 #1",
|
||||
"customer_name": "옥혜진",
|
||||
"inquiry_content": "타인 계정 문의 3 입니다. 스코프 격리 검증용.",
|
||||
"inquiry_url": "https://api.example.com/admin/ecommerce/product-inquiries",
|
||||
"site_url": "https://api.example.com"
|
||||
}
|
||||
},
|
||||
"read_at": null,
|
||||
"created_at": "2026-07-31 21:41:58"
|
||||
},
|
||||
{
|
||||
"number": 22,
|
||||
"id": "75da60b8-3ec9-457f-ad8c-b52652f7adcf",
|
||||
"type": "inquiry_received",
|
||||
"type_label": "상품 문의 접수",
|
||||
"subject": "새로운 상품 문의가 접수되었습니다",
|
||||
"body": "옥혜진님이 \"면 손수건 3매입 #1\" 상품에 문의를 남겼습니다.",
|
||||
"url": "https://api.example.com/admin/ecommerce/product-inquiries",
|
||||
"data": {
|
||||
"type": "inquiry_received",
|
||||
"subject": "새로운 상품 문의가 접수되었습니다",
|
||||
"body": "옥혜진님이 \"면 손수건 3매입 #1\" 상품에 문의를 남겼습니다.",
|
||||
"click_url": "https://api.example.com/admin/ecommerce/product-inquiries",
|
||||
"data": {
|
||||
"name": "최고관리자",
|
||||
"app_name": "그누보드7",
|
||||
"product_name": "면 손수건 3매입 #1",
|
||||
"customer_name": "옥혜진",
|
||||
"inquiry_content": "타인 계정 문의 2 입니다. 스코프 격리 검증용.",
|
||||
"inquiry_url": "https://api.example.com/admin/ecommerce/product-inquiries",
|
||||
"site_url": "https://api.example.com"
|
||||
}
|
||||
},
|
||||
"read_at": null,
|
||||
"created_at": "2026-07-31 21:41:55"
|
||||
},
|
||||
"... (총 23건 중 2건 표시)"
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"per_page": 25,
|
||||
"total": 0,
|
||||
"from": null,
|
||||
"to": null,
|
||||
"total": 23,
|
||||
"from": 1,
|
||||
"to": 23,
|
||||
"has_more_pages": false
|
||||
}
|
||||
}
|
||||
@@ -114,7 +179,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| deleted_count | integer | `0` | deleted 개수 (집계) |
|
||||
| deleted_count | integer | `23` | deleted 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -127,7 +192,7 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "모든 알림이 삭제되었습니다.",
|
||||
"data": {
|
||||
"deleted_count": 0
|
||||
"deleted_count": 23
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -171,7 +236,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| marked_count | integer | `0` | marked 개수 (집계) |
|
||||
| marked_count | integer | `7` | marked 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -184,7 +249,7 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "모든 알림을 읽음 처리했습니다.",
|
||||
"data": {
|
||||
"marked_count": 0
|
||||
"marked_count": 7
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -235,7 +300,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -281,7 +346,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| unread_count | integer | `0` | unread 개수 (집계) |
|
||||
| unread_count | integer | `7` | unread 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -294,7 +359,7 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "미읽음 알림 수를 조회했습니다.",
|
||||
"data": {
|
||||
"unread_count": 0
|
||||
"unread_count": 7
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -336,7 +401,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -380,7 +445,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -431,7 +496,18 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
|
||||
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| number | integer | `23` | 목록에서의 순번 (페이지네이션 반영 행 번호 — HasRowNumber 파생) |
|
||||
| id | string | `518b9c94-5853-4b68-8193-83af1851bbc6` | 기본 키 (내부 식별자) |
|
||||
| type | string | `inquiry_received` | <!-- TODO: 설명 --> |
|
||||
| type_label | string | `상품 문의 접수` | `type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) |
|
||||
| subject | string | `새로운 상품 문의가 접수되었습니다` | <!-- TODO: 설명 --> |
|
||||
| body | string | `옥혜진님이 "면 손수건 3매입 #1" 상품에 문의를 남겼습니다.` | <!-- TODO: 설명 --> |
|
||||
| url | string | `https://g7_2.dev/admin/ecommerce/prod…` | <!-- TODO: 설명 --> |
|
||||
| data | object | `{"type":"inquiry_received","subject":"새로운 상품 문의가 접수되었습니다"…` | <!-- TODO: 설명 --> |
|
||||
| read_at | string | `2026-07-31 22:06:53` | read 일시 |
|
||||
| created_at | string | `2026-07-31 21:41:58` | 생성 일시 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -444,14 +520,68 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "알림 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"data": [],
|
||||
"data": [
|
||||
{
|
||||
"number": 23,
|
||||
"id": "518b9c94-5853-4b68-8193-83af1851bbc6",
|
||||
"type": "inquiry_received",
|
||||
"type_label": "상품 문의 접수",
|
||||
"subject": "새로운 상품 문의가 접수되었습니다",
|
||||
"body": "옥혜진님이 \"면 손수건 3매입 #1\" 상품에 문의를 남겼습니다.",
|
||||
"url": "https://api.example.com/admin/ecommerce/product-inquiries",
|
||||
"data": {
|
||||
"type": "inquiry_received",
|
||||
"subject": "새로운 상품 문의가 접수되었습니다",
|
||||
"body": "옥혜진님이 \"면 손수건 3매입 #1\" 상품에 문의를 남겼습니다.",
|
||||
"click_url": "https://api.example.com/admin/ecommerce/product-inquiries",
|
||||
"data": {
|
||||
"name": "최고관리자",
|
||||
"app_name": "그누보드7",
|
||||
"product_name": "면 손수건 3매입 #1",
|
||||
"customer_name": "옥혜진",
|
||||
"inquiry_content": "타인 계정 문의 3 입니다. 스코프 격리 검증용.",
|
||||
"inquiry_url": "https://api.example.com/admin/ecommerce/product-inquiries",
|
||||
"site_url": "https://api.example.com"
|
||||
}
|
||||
},
|
||||
"read_at": null,
|
||||
"created_at": "2026-07-31 21:41:58"
|
||||
},
|
||||
{
|
||||
"number": 22,
|
||||
"id": "75da60b8-3ec9-457f-ad8c-b52652f7adcf",
|
||||
"type": "inquiry_received",
|
||||
"type_label": "상품 문의 접수",
|
||||
"subject": "새로운 상품 문의가 접수되었습니다",
|
||||
"body": "옥혜진님이 \"면 손수건 3매입 #1\" 상품에 문의를 남겼습니다.",
|
||||
"url": "https://api.example.com/admin/ecommerce/product-inquiries",
|
||||
"data": {
|
||||
"type": "inquiry_received",
|
||||
"subject": "새로운 상품 문의가 접수되었습니다",
|
||||
"body": "옥혜진님이 \"면 손수건 3매입 #1\" 상품에 문의를 남겼습니다.",
|
||||
"click_url": "https://api.example.com/admin/ecommerce/product-inquiries",
|
||||
"data": {
|
||||
"name": "최고관리자",
|
||||
"app_name": "그누보드7",
|
||||
"product_name": "면 손수건 3매입 #1",
|
||||
"customer_name": "옥혜진",
|
||||
"inquiry_content": "타인 계정 문의 2 입니다. 스코프 격리 검증용.",
|
||||
"inquiry_url": "https://api.example.com/admin/ecommerce/product-inquiries",
|
||||
"site_url": "https://api.example.com"
|
||||
}
|
||||
},
|
||||
"read_at": null,
|
||||
"created_at": "2026-07-31 21:41:55"
|
||||
},
|
||||
"... (총 23건 중 2건 표시)"
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"per_page": 25,
|
||||
"total": 0,
|
||||
"from": null,
|
||||
"to": null,
|
||||
"total": 23,
|
||||
"from": 1,
|
||||
"to": 23,
|
||||
"has_more_pages": false
|
||||
}
|
||||
}
|
||||
@@ -497,7 +627,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| deleted_count | integer | `0` | deleted 개수 (집계) |
|
||||
| deleted_count | integer | `23` | deleted 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -510,7 +640,7 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "모든 알림이 삭제되었습니다.",
|
||||
"data": {
|
||||
"deleted_count": 0
|
||||
"deleted_count": 23
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -553,7 +683,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| marked_count | integer | `0` | marked 개수 (집계) |
|
||||
| marked_count | integer | `7` | marked 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -566,7 +696,7 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "모든 알림을 읽음 처리했습니다.",
|
||||
"data": {
|
||||
"marked_count": 0
|
||||
"marked_count": 7
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -616,7 +746,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -661,7 +791,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| unread_count | integer | `0` | unread 개수 (집계) |
|
||||
| unread_count | integer | `7` | unread 개수 (집계) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -674,7 +804,7 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "미읽음 알림 수를 조회했습니다.",
|
||||
"data": {
|
||||
"unread_count": 0
|
||||
"unread_count": 7
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -715,7 +845,7 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -758,7 +888,7 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Password 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -51,7 +51,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
+54
-4459
File diff suppressed because it is too large
Load Diff
+572
-100
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Plugins 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -66,7 +66,7 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
| is_installed | boolean | `false` | installed 여부 |
|
||||
| has_settings | boolean | `true` | settings 여부 |
|
||||
| settings_route | string | `/admin/plugins/sirsoft-ckeditor5/sett…` | 설정 페이지 경로 (설정 UI 진입 라우트, 설정 미제공 시 null) |
|
||||
| assets | object | `{"js":"\/api\/plugins\/assets\/sirsoft-ckeditor5\/dist\/j…` | 프론트엔드 에셋 매니페스트 (manifest 파생 — js/css 진입점·로딩 전략) |
|
||||
| assets | object | `{"js":"\/api\/plugins\/assets\/sirsoft-ckeditor5?file=dis…` | 프론트엔드 에셋 매니페스트 (manifest 파생 — js/css 진입점·로딩 전략) |
|
||||
| update_available | boolean | `false` | 최신 버전 대비 업데이트 가능 여부 |
|
||||
| update_source | null | `null` | 업데이트 감지 출처 (github, bundled 등) |
|
||||
| latest_version | string | `1.0.0` | 감지된 최신 배포 버전 |
|
||||
@@ -104,21 +104,21 @@ HTTP/1.1 200
|
||||
"roles": [],
|
||||
"config": [],
|
||||
"hooks": [],
|
||||
"status": "uninstalled",
|
||||
"status": "active",
|
||||
"is_installed": false,
|
||||
"has_settings": true,
|
||||
"settings_route": "/admin/plugins/sirsoft-ckeditor5/settings",
|
||||
"assets": {
|
||||
"js": "/api/plugins/assets/sirsoft-ckeditor5/dist/js/plugin.iife.js",
|
||||
"js": "/api/plugins/assets/sirsoft-ckeditor5?file=dist%2Fjs%2Fplugin.iife.js",
|
||||
"css": null,
|
||||
"priority": 100
|
||||
},
|
||||
"update_available": false,
|
||||
"update_source": null,
|
||||
"latest_version": null,
|
||||
"file_version": null,
|
||||
"github_url": null,
|
||||
"github_changelog_url": null,
|
||||
"latest_version": "1.0.0",
|
||||
"file_version": "1.0.0",
|
||||
"github_url": "https://github.com/gnuboard/g7-plugin-sirsoft-ckeditor5",
|
||||
"github_changelog_url": "https://github.com/gnuboard/g7-plugin-sirsoft-ckeditor5/releases",
|
||||
"is_pending": false,
|
||||
"is_bundled": false,
|
||||
"deactivated_reason": null,
|
||||
@@ -135,28 +135,28 @@ HTTP/1.1 200
|
||||
"identifier": "sirsoft-daum_postcode",
|
||||
"vendor": "sirsoft",
|
||||
"name": "Daum 우편번호",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Daum 우편번호 서비스를 통한 주소 검색 기능을 제공하는 플러그인입니다. API 키 없이 무료로 사용할 수 있습니다.",
|
||||
"dependencies": [],
|
||||
"permissions": [],
|
||||
"roles": [],
|
||||
"config": [],
|
||||
"hooks": [],
|
||||
"status": "uninstalled",
|
||||
"status": "active",
|
||||
"is_installed": false,
|
||||
"has_settings": true,
|
||||
"settings_route": "/admin/plugins/sirsoft-daum_postcode/settings",
|
||||
"assets": {
|
||||
"js": "/api/plugins/assets/sirsoft-daum_postcode/dist/js/plugin.iife.js",
|
||||
"js": "/api/plugins/assets/sirsoft-daum_postcode?file=dist%2Fjs%2Fplugin.iife.js",
|
||||
"css": null,
|
||||
"priority": 100
|
||||
},
|
||||
"update_available": false,
|
||||
"update_source": null,
|
||||
"latest_version": null,
|
||||
"file_version": null,
|
||||
"github_url": null,
|
||||
"github_changelog_url": null,
|
||||
"latest_version": "1.0.1",
|
||||
"file_version": "1.0.1",
|
||||
"github_url": "https://github.com/gnuboard/g7-plugin-sirsoft-daum_postcode",
|
||||
"github_changelog_url": "https://github.com/gnuboard/g7-plugin-sirsoft-daum_postcode/releases",
|
||||
"is_pending": false,
|
||||
"is_bundled": false,
|
||||
"deactivated_reason": null,
|
||||
@@ -168,20 +168,20 @@ HTTP/1.1 200
|
||||
"can_uninstall": true
|
||||
}
|
||||
},
|
||||
"... (총 9건 중 2건 표시)"
|
||||
"... (총 10건 중 2건 표시)"
|
||||
],
|
||||
"pagination": {
|
||||
"total": 9,
|
||||
"total": 10,
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"per_page": 25
|
||||
},
|
||||
"meta": {
|
||||
"total_plugins": 9,
|
||||
"active_plugins": 0,
|
||||
"total_plugins": 10,
|
||||
"active_plugins": 8,
|
||||
"inactive_plugins": 0,
|
||||
"installed_plugins": 0,
|
||||
"uninstalled_plugins": 9
|
||||
"installed_plugins": 8,
|
||||
"uninstalled_plugins": 2
|
||||
},
|
||||
"abilities": {
|
||||
"can_install": true,
|
||||
@@ -237,7 +237,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -277,7 +277,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -327,7 +327,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -386,7 +386,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -438,7 +438,7 @@ Content-Type: application/octet-stream
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -487,7 +487,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -546,7 +546,7 @@ _목록 응답: `data.data[]` 배열 항목의 필드._
|
||||
| is_installed | boolean | `false` | installed 여부 |
|
||||
| has_settings | boolean | `true` | settings 여부 |
|
||||
| settings_route | string | `/admin/plugins/sirsoft-ckeditor5/sett…` | 설정 페이지 경로 (설정 UI 진입 라우트, 설정 미제공 시 null) |
|
||||
| assets | object | `{"js":"\/api\/plugins\/assets\/sirsoft-ckeditor5\/dist\/j…` | 프론트엔드 에셋 매니페스트 (manifest 파생 — js/css 진입점·로딩 전략) |
|
||||
| assets | object | `{"js":"\/api\/plugins\/assets\/sirsoft-ckeditor5?file=dis…` | 프론트엔드 에셋 매니페스트 (manifest 파생 — js/css 진입점·로딩 전략) |
|
||||
| update_available | boolean | `false` | 최신 버전 대비 업데이트 가능 여부 |
|
||||
| update_source | null | `null` | 업데이트 감지 출처 (github, bundled 등) |
|
||||
| latest_version | string | `1.0.0` | 감지된 최신 배포 버전 |
|
||||
@@ -571,12 +571,90 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "플러그인 목록을 성공적으로 가져왔습니다.",
|
||||
"data": {
|
||||
"data": [],
|
||||
"data": [
|
||||
{
|
||||
"id": null,
|
||||
"identifier": "sirsoft-ckeditor5",
|
||||
"vendor": "sirsoft",
|
||||
"name": "CKEditor 5 WYSIWYG 에디터",
|
||||
"version": "1.0.0",
|
||||
"description": "CKEditor 5를 이용한 WYSIWYG 에디터 플러그인입니다. 플러그인 설치만으로 기존 HtmlEditor가 교체됩니다.",
|
||||
"dependencies": [],
|
||||
"permissions": [],
|
||||
"roles": [],
|
||||
"config": [],
|
||||
"hooks": [],
|
||||
"status": "active",
|
||||
"is_installed": false,
|
||||
"has_settings": true,
|
||||
"settings_route": "/admin/plugins/sirsoft-ckeditor5/settings",
|
||||
"assets": {
|
||||
"js": "/api/plugins/assets/sirsoft-ckeditor5?file=dist%2Fjs%2Fplugin.iife.js",
|
||||
"css": null,
|
||||
"priority": 100
|
||||
},
|
||||
"update_available": false,
|
||||
"update_source": null,
|
||||
"latest_version": "1.0.0",
|
||||
"file_version": "1.0.0",
|
||||
"github_url": "https://github.com/gnuboard/g7-plugin-sirsoft-ckeditor5",
|
||||
"github_changelog_url": "https://github.com/gnuboard/g7-plugin-sirsoft-ckeditor5/releases",
|
||||
"is_pending": false,
|
||||
"is_bundled": false,
|
||||
"deactivated_reason": null,
|
||||
"deactivated_at": null,
|
||||
"incompatible_required_version": null,
|
||||
"abilities": {
|
||||
"can_install": true,
|
||||
"can_activate": true,
|
||||
"can_uninstall": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": null,
|
||||
"identifier": "sirsoft-daum_postcode",
|
||||
"vendor": "sirsoft",
|
||||
"name": "Daum 우편번호",
|
||||
"version": "1.0.1",
|
||||
"description": "Daum 우편번호 서비스를 통한 주소 검색 기능을 제공하는 플러그인입니다. API 키 없이 무료로 사용할 수 있습니다.",
|
||||
"dependencies": [],
|
||||
"permissions": [],
|
||||
"roles": [],
|
||||
"config": [],
|
||||
"hooks": [],
|
||||
"status": "active",
|
||||
"is_installed": false,
|
||||
"has_settings": true,
|
||||
"settings_route": "/admin/plugins/sirsoft-daum_postcode/settings",
|
||||
"assets": {
|
||||
"js": "/api/plugins/assets/sirsoft-daum_postcode?file=dist%2Fjs%2Fplugin.iife.js",
|
||||
"css": null,
|
||||
"priority": 100
|
||||
},
|
||||
"update_available": false,
|
||||
"update_source": null,
|
||||
"latest_version": "1.0.1",
|
||||
"file_version": "1.0.1",
|
||||
"github_url": "https://github.com/gnuboard/g7-plugin-sirsoft-daum_postcode",
|
||||
"github_changelog_url": "https://github.com/gnuboard/g7-plugin-sirsoft-daum_postcode/releases",
|
||||
"is_pending": false,
|
||||
"is_bundled": false,
|
||||
"deactivated_reason": null,
|
||||
"deactivated_at": null,
|
||||
"incompatible_required_version": null,
|
||||
"abilities": {
|
||||
"can_install": true,
|
||||
"can_activate": true,
|
||||
"can_uninstall": true
|
||||
}
|
||||
},
|
||||
"... (총 8건 중 2건 표시)"
|
||||
],
|
||||
"meta": {
|
||||
"total_plugins": 0,
|
||||
"active_plugins": 0,
|
||||
"total_plugins": 8,
|
||||
"active_plugins": 8,
|
||||
"inactive_plugins": 0,
|
||||
"installed_plugins": 0,
|
||||
"installed_plugins": 8,
|
||||
"uninstalled_plugins": 0
|
||||
}
|
||||
}
|
||||
@@ -625,7 +703,7 @@ Content-Type: application/octet-stream
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -674,7 +752,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -719,7 +797,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -753,12 +831,10 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
| from_version | query | string | 아니오 | — | 시작 버전 (범위 하한) |
|
||||
| to_version | query | string | 아니오 | — | 대상 버전 (범위 상한) |
|
||||
|
||||
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.extension.changelog_validation_rules`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/plugins/{identifier}/changelog?source=active&from_version=%EC%98%88%EC%8B%9C%EA%B0%92&to_version=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1
|
||||
GET /api/admin/plugins/sirsoft-daum_postcode/changelog?source=active&from_version=%EC%98%88%EC%8B%9C%EA%B0%92&to_version=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -766,11 +842,149 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| changelog | array | `[{"version":"1.0.1","date":"2026-07-22","categories":[{"n…` | 변경 이력 텍스트 (원격/파일 CHANGELOG 본문) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "플러그인 정보를 성공적으로 가져왔습니다.",
|
||||
"data": {
|
||||
"changelog": [
|
||||
{
|
||||
"version": "1.0.1",
|
||||
"date": "2026-07-22",
|
||||
"categories": [
|
||||
{
|
||||
"name": "Fixed",
|
||||
"items": [
|
||||
"오류 안내가 뜨기는 하지만 내용이 비어 있던 문제를 수정했습니다. 설정 저장에 실패하면 서버가 알려 준 사유가 그대로 표시됩니다.",
|
||||
"설정 화면의 아이콘이 의도한 크기보다 크거나 작게 보이던 문제를 수정했습니다."
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"date": "2026-07-01",
|
||||
"categories": [
|
||||
{
|
||||
"name": "Added",
|
||||
"items": [
|
||||
"레이아웃 편집기 데이터 소스 목록에서 이 확장이 제공하는 데이터 소스가 친화 명칭으로 표시되고, 어느 확장이 제공했는지 출처가 함께 표시됩니다."
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Changed",
|
||||
"items": [
|
||||
"플러그인 환경설정 화면의 하단 저장 버튼이 스크롤 중에도 화면에 고정되도록 개선."
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.0-beta.2",
|
||||
"date": "2026-04-20",
|
||||
"categories": [
|
||||
{
|
||||
"name": "Changed",
|
||||
"items": [
|
||||
"주소 검색 영역의 콘텐츠 카드 / 세로 정렬 컨테이너 외형을 sirsoft-admin_basic 표준 시맨틱과 정합 — 다른 화면과 같은 결로 통일.",
|
||||
"코어 최소 요구 버전을 7.0.0-beta.2 로 상향",
|
||||
"extension JSON: `extensionPointProps.onAddressSelect` → `extensionPointCallbacks.onAddressSelect` 참조 변경 (extension_point props/callbacks 분리)",
|
||||
"플러그인 환경설정 화면의 하단 저장 버튼이 스크롤 중에도 화면에 고정되도록 개선.",
|
||||
"플러그인 환경설정 화면의 폼 라벨 / 보조 설명 / 에러 메시지 시각 시맨틱을 sirsoft-admin_basic 표준 시맨틱과 정합 — 다른 관리자 화면과 같은 결로 통일.",
|
||||
"플러그인 환경설정 화면 곳곳의 텍스트 톤 (보조 설명 · 라벨 · 본문 · 강조 · 작은 보조) 시각 시맨틱을 관리자 표준 시맨틱과 정합 — 같은 결의 글자 톤이 한 곳에서 일괄 조정 가능.",
|
||||
"플러그인 환경설정 화면의 세로 정렬 컨테이너 / 입력 박스를 sirsoft-admin_basic 표준 시맨틱 (.stack / .input) 과 정합 — 표준 간격과 외형이 일관 표시되도록 정리."
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.0-beta.1",
|
||||
"date": "2026-04-01",
|
||||
"categories": [
|
||||
{
|
||||
"name": "Changed",
|
||||
"items": [
|
||||
"오픈 베타 릴리즈"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.1.3",
|
||||
"date": "2026-03-16",
|
||||
"categories": [
|
||||
{
|
||||
"name": "Changed",
|
||||
"items": [
|
||||
"라이선스 프로그램 명칭 정비"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.1.2",
|
||||
"date": "2026-03-13",
|
||||
"categories": [
|
||||
{
|
||||
"name": "Added",
|
||||
"items": [
|
||||
"manifest에 license 필드 및 LICENSE 파일 추가"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Changed",
|
||||
"items": [
|
||||
"설정 레이아웃 경로를 `resources/layouts/settings.json` → `resources/layouts/admin/plugin_settings.json`으로 이동 (모듈과 동일한 구조 통일)"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.1.1",
|
||||
"date": "2026-02-24",
|
||||
"categories": [
|
||||
{
|
||||
"name": "Changed",
|
||||
"items": [
|
||||
"버전 체계 조정 (정식 출시 전 0.x 체계로 변경)"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"date": "2026-02-23",
|
||||
"categories": [
|
||||
{
|
||||
"name": "Added",
|
||||
"items": [
|
||||
"Daum 우편번호 검색 플러그인 초기 구현",
|
||||
"Daum 우편번호 서비스 API 연동 (API 키 불필요)",
|
||||
"주소 검색 팝업/레이어 표시 모드 설정",
|
||||
"커스텀 핸들러 (openPostcode, setFieldReadOnly)",
|
||||
"이커머스 주소 검색 레이아웃 확장 (ecommerce-address-search)",
|
||||
"플러그인 설정 UI (표시 모드, 테마 설정)",
|
||||
"다국어 지원 (ko, en)"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -801,7 +1015,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/plugins/{identifier}/dependent-templates HTTP/1.1
|
||||
GET /api/admin/plugins/sirsoft-daum_postcode/dependent-templates HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -809,11 +1023,42 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| identifier | string | `sirsoft-basic` | 플러그인 고유 식별자 (vendor-plugin 형식) |
|
||||
| name | string | `Basic` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| version | string | `1.0.4` | 플러그인 버전 |
|
||||
| type | string | `user` | <!-- TODO: 설명 --> |
|
||||
| status | string | `active` | 상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조) |
|
||||
| required_version | string | `>=1.0.0` | <!-- TODO: 설명 --> |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "의존 템플릿 정보를 성공적으로 조회했습니다.",
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"identifier": "sirsoft-basic",
|
||||
"name": "Basic",
|
||||
"version": "1.0.4",
|
||||
"type": "user",
|
||||
"status": "active",
|
||||
"required_version": ">=1.0.0"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -843,7 +1088,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/plugins/{identifier}/license HTTP/1.1
|
||||
GET /api/admin/plugins/sirsoft-daum_postcode/license HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -851,11 +1096,27 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| content | string | `프로그램 명칭 : 그누보드7용 Daum 우편번호 플러그인 (sirs…` | 본문 내용 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "플러그인 정보를 성공적으로 가져왔습니다.",
|
||||
"data": {
|
||||
"content": "프로그램 명칭 : 그누보드7용 Daum 우편번호 플러그인 (sirsoft-daum_postcode)\n\n저작자 : (주)에스아이알소프트\n\n----- MIT 라이선스 (한국어 번역) --------------------------------------------------------\n\nMIT 라이선스\n\nCopyright (c) 2026 (주)에스아이알소프트\n\n이 소프트웨어와 관련 문서 파일(이하 \"소프트웨어\")의 복사본을 취득하는 모든 사람에게\n소프트웨어를 제한 없이 사용, 복사, 수정, 병합, 출판, 배포, 서브라이선스 허여 및/또는\n판매할 수 있는 권리를 무상으로 부여합니다. 다만, 소프트웨어를 제공받은 사람은 다음\n조건을 따라야 합니다:\n\n위 저작권 고지와 본 허가 고지는 소프트웨어의 모든 복사본 또는 상당 부분에 포함되어야\n합니다.\n\n소프트웨어는 \"있는 그대로\" 제공되며, 명시적이든 묵시적이든 어떠한 종류의 보증도 하지\n않습니다. 여기에는 상품성, 특정 목적에의 적합성 및 비침해에 대한 보증이 포함되나 이에\n국한되지 않습니다. 어떠한 경우에도 저작자 또는 저작권자는 소프트웨어나 소프트웨어의\n사용 또는 기타 거래로 인해 발생하는 계약, 불법행위 또는 기타 청구, 손해 또는 기타\n책임에 대해 책임을 지지 않습니다.\n\n----- MIT License (English Original) --------------------------------------------------------\n\nThe MIT License (MIT)\n\nCopyright (c) 2026 SIRSOFT\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -885,7 +1146,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/plugins/{identifier}/settings HTTP/1.1
|
||||
GET /api/admin/plugins/sirsoft-daum_postcode/settings HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -893,11 +1154,33 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| display_mode | string | `layer` | <!-- TODO: 설명 --> |
|
||||
| popup_width | integer | `500` | <!-- TODO: 설명 --> |
|
||||
| popup_height | integer | `600` | <!-- TODO: 설명 --> |
|
||||
| theme_color | string | `#1D4ED8` | <!-- TODO: 설명 --> |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "messages.success",
|
||||
"data": {
|
||||
"display_mode": "layer",
|
||||
"popup_width": 500,
|
||||
"popup_height": 600,
|
||||
"theme_color": "#1D4ED8"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -924,12 +1207,10 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| identifier | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.plugin_settings.update_validation_rules`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PUT /api/admin/plugins/{identifier}/settings HTTP/1.1
|
||||
PUT /api/admin/plugins/sirsoft-daum_postcode/settings HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -937,7 +1218,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -949,7 +1230,6 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
| --- | --- | --- |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.plugins.update`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
@@ -972,7 +1252,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/admin/plugins/{identifier}/settings/layout HTTP/1.1
|
||||
GET /api/admin/plugins/sirsoft-daum_postcode/settings/layout HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -980,11 +1260,45 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| version | string | `1.0.0` | 플러그인 버전 |
|
||||
| layout_name | string | `plugin_settings` | <!-- TODO: 설명 --> |
|
||||
| permissions | array | `["core.plugins.update"]` | 권한 목록 (각 원소 id/identifier/name — permissions 관계 또는 역할 경유 파생) |
|
||||
| extends | string | `_admin_base` | <!-- TODO: 설명 --> |
|
||||
| meta | object | `{"title":"$t:sirsoft-daum_postcode.settings.title","descr…` | <!-- TODO: 설명 --> |
|
||||
| data_sources | array | `[{"id":"settings","label_key":"$t:sirsoft-daum_postcode.e…` | <!-- TODO: 설명 --> |
|
||||
| slots | object | `{"content":[{"id":"plugin_settings_content","type":"basic…` | <!-- TODO: 설명 --> |
|
||||
| pageConfig | object | `{"notice":"$t:sirsoft-daum_postcode.settings.notice","gui…` | <!-- TODO: 설명 --> |
|
||||
| schema | object | `{"display_mode":{"type":"enum","options":["popup","layer"…` | <!-- TODO: 설명 --> |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "messages.success",
|
||||
"data": {
|
||||
"version": "1.0.0",
|
||||
"layout_name": "plugin_settings",
|
||||
"permissions": [
|
||||
"core.plugins.update"
|
||||
],
|
||||
"extends": "_admin_base",
|
||||
"meta": {
|
||||
"title": "$t:sirsoft-daum_postcode.settings.title",
|
||||
"description": "$t:sirsoft-daum_postcode.settings.description"
|
||||
},
|
||||
"...": "(4개 키 생략, 총 9개)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -1064,38 +1378,11 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
| --- | --- | --- |
|
||||
| has_modified_layouts | boolean | 수정된 레이아웃 존재 여부 |
|
||||
| modified_count | integer | 수정된 레이아웃 수 |
|
||||
| modified_layouts | array | 수정된 레이아웃 목록 |
|
||||
| modified_layouts[].id | integer | 레이아웃 ID |
|
||||
| modified_layouts[].name | string | 레이아웃 이름 |
|
||||
| modified_layouts[].updated_at | string\|null | 최종 수정 일시 (`Y-m-d H:i:s`) |
|
||||
| modified_layouts[].size_diff | integer | 원본 대비 바이트 증감 (음수는 축소) |
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "수정된 레이아웃 확인이 완료되었습니다.",
|
||||
"data": {
|
||||
"has_modified_layouts": true,
|
||||
"modified_count": 1,
|
||||
"modified_layouts": [
|
||||
{
|
||||
"id": 47,
|
||||
"name": "sirsoft-board.admin_board_index",
|
||||
"updated_at": "2026-07-31 09:20:05",
|
||||
"size_diff": 28
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
집계 범위는 해당 플러그인이 **소유한** 레이아웃으로 한정됩니다. 같은 템플릿에 등록된 다른 확장의 레이아웃은 포함되지 않습니다.
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -1225,13 +1512,13 @@ Content-Type: application/json
|
||||
"layout_strategy": "overwrite",
|
||||
"vendor_mode": "auto",
|
||||
"force": true,
|
||||
"rebuild_search_index": false
|
||||
"rebuild_search_index": true
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1251,6 +1538,47 @@ Content-Type: application/json
|
||||
**설명** 특정 플러그인을 최신 버전으로 업데이트합니다. `core.plugins.install` 권한이 필요합니다. `layout_strategy` 로 레이아웃 처리 방식을(overwrite: 새 버전으로 교체, keep: 사용자 수정본 유지) 지정하며, `vendor_mode` 로 Composer 의존성 처리 방식을 선택합니다. 버전 제약·호환성 문제로 막힐 경우 `force: true` 로 강제 진행할 수 있습니다. `keep` 을 지정하면 사용자가 수정한 레이아웃(원본 해시와 현재 내용이 다른 레이아웃)은 갱신 대상에서 제외되어 현재 내용이 그대로 유지되고, 나머지 레이아웃만 파일 기준으로 갱신됩니다. 성공 응답 메시지에는 대상 식별자와 적용된 버전이 채워집니다. `rebuild_search_index: true` 를 함께 보내면 업데이트 후 색인이 누락된 검색 인덱스를 재생성합니다 — 인덱스 잠금·재색인 비용이 있어 기본은 수행하지 않으며, 보내지 않아도 응답의 `search_index` 에 현재 누락 여부가 담깁니다.
|
||||
|
||||
|
||||
### GET /api/plugins/assets/{identifier}
|
||||
<!-- @generated:start:api.public.plugins.assets.extensionless -->
|
||||
- **라우트명**: `api.public.plugins.assets.extensionless`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicPluginController@serveAsset`
|
||||
- **인증/권한**: 공개 (인증 불필요)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| identifier | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
| path | query | string | 예 | — | 경로 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/plugins/assets/sirsoft-daum_postcode?identifier=example-key&path=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 -->
|
||||
|
||||
|
||||
### GET /api/plugins/assets/{identifier}/{path}
|
||||
<!-- @generated:start:api.public.plugins.assets -->
|
||||
- **라우트명**: `api.public.plugins.assets`
|
||||
@@ -1267,7 +1595,7 @@ Content-Type: application/json
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/plugins/assets/{identifier}/{path} HTTP/1.1
|
||||
GET /api/plugins/assets/sirsoft-daum_postcode/{path}?identifier=example-key HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
```
|
||||
@@ -1312,11 +1640,11 @@ Accept: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: http-200 — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-200 — 응답 예시는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -1347,11 +1675,11 @@ Accept: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: http-200 — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-200 — 응답 예시는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -1362,6 +1690,132 @@ _대표 에러 없음 (공개 조회 — 활성 에셋이 없으면 빈 200 응
|
||||
**설명** 활성 플러그인들의 프론트엔드 IIFE JS 를 서버에서 하나로 병합한 번들을 서빙하는 공개 엔드포인트입니다. 인증이 필요하지 않습니다. 활성 global 플러그인 에셋이 없으면 빈 200(text/javascript) 응답을 반환하고, 있으면 병합 파일을 ETag·환경별 Cache-Control 과 함께 서빙합니다. 프론트는 `G7Config.bundleUrls` 를 읽어 이 번들을 로드합니다.
|
||||
|
||||
|
||||
### GET /api/plugins/bundle/css
|
||||
<!-- @generated:start:api.public.plugins.bundle.css.extensionless -->
|
||||
- **라우트명**: `api.public.plugins.bundle.css.extensionless`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicPluginController@serveBundleCss`
|
||||
- **인증/권한**: 공개 (인증 불필요)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/plugins/bundle/css HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: http-200 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-200 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 -->
|
||||
|
||||
|
||||
### GET /api/plugins/bundle/js
|
||||
<!-- @generated:start:api.public.plugins.bundle.js.extensionless -->
|
||||
- **라우트명**: `api.public.plugins.bundle.js.extensionless`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicPluginController@serveBundleJs`
|
||||
- **인증/권한**: 공개 (인증 불필요)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/plugins/bundle/js HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: http-200 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-200 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 -->
|
||||
|
||||
|
||||
### GET /api/plugins/{identifier}/components
|
||||
<!-- @generated:start:api.public.plugins.components.extensionless -->
|
||||
- **라우트명**: `api.public.plugins.components.extensionless`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Public\PublicPluginController@serveComponents`
|
||||
- **인증/권한**: 공개 (인증 불필요)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| identifier | path | string | 예 | — | 대상 리소스의 식별자 |
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/plugins/sirsoft-daum_postcode/components HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
|
||||
|
||||
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"identifier": "sirsoft-daum_postcode",
|
||||
"version": "1.0.0",
|
||||
"components": {
|
||||
"basic": [],
|
||||
"composite": [],
|
||||
"layout": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 -->
|
||||
|
||||
|
||||
### GET /api/plugins/{identifier}/components.json
|
||||
<!-- @generated:start:api.public.plugins.components -->
|
||||
- **라우트명**: `api.public.plugins.components`
|
||||
@@ -1377,18 +1831,18 @@ _대표 에러 없음 (공개 조회 — 활성 에셋이 없으면 빈 200 응
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/plugins/{identifier}/components.json HTTP/1.1
|
||||
GET /api/plugins/sirsoft-daum_postcode/components.json HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -1416,18 +1870,36 @@ Accept: application/json
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/plugins/{identifier}/editor-spec HTTP/1.1
|
||||
GET /api/plugins/sirsoft-daum_postcode/editor-spec HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| identifier | string | `sirsoft-daum_postcode` | 플러그인 고유 식별자 (vendor-plugin 형식) |
|
||||
| spec | null | `null` | 스펙 정의 객체 (편집기/컴포넌트 선언 스키마 등) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "편집기 스펙이 비어 있습니다.",
|
||||
"data": {
|
||||
"identifier": "sirsoft-daum_postcode",
|
||||
"spec": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Profile 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -123,13 +123,17 @@ Content-Type: application/json
|
||||
"notify_post_complete": true,
|
||||
"notify_post_reply": true,
|
||||
"notify_comment": true,
|
||||
"notify_reply_comment": true
|
||||
"notify_reply_comment": true,
|
||||
"email_subscription": true,
|
||||
"marketing_consent": true,
|
||||
"third_party_consent": true,
|
||||
"info_disclosure": true
|
||||
}
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -210,7 +214,7 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
+153
-8870
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Schedules 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -73,11 +73,11 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
| is_active | boolean | `true` | active 여부 |
|
||||
| last_result | string | `success` | 마지막 실행 결과: success(성공), failed(실패), running(실행중), never(미실행) |
|
||||
| last_result_label | string | `성공` | `last_result` 값의 사람이 읽는 라벨 (현지화/Enum 파생) |
|
||||
| last_run_at | string | `2026-07-05T19:20:23+09:00` | last run 일시 |
|
||||
| last_run_at | string | `2026-07-30T22:55:00+09:00` | last run 일시 |
|
||||
| last_duration | null | `null` | 마지막 실행의 소요 시간을 사람이 읽는 문자열로 포맷한 값 (예: "45초", "2분 3초" — 마지막 실행 이력의 duration 파생, 실행 이력이 없으면 null) |
|
||||
| next_run_at | string | `2026-07-07T12:00:00+09:00` | next run 일시 |
|
||||
| creator | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) |
|
||||
| created_at | string | `2026-07-06` | 생성 일시 |
|
||||
| next_run_at | string | `2026-08-01T12:00:00+09:00` | next run 일시 |
|
||||
| creator | object | `{"uuid":"a2640dae-e87c-4a28-b4f1-481fd961dc02","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) |
|
||||
| created_at | string | `2026-07-31` | 생성 일시 |
|
||||
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -106,14 +106,14 @@ HTTP/1.1 200
|
||||
"is_active": true,
|
||||
"last_result": "success",
|
||||
"last_result_label": "성공",
|
||||
"last_run_at": "2026-07-07T10:41:24+09:00",
|
||||
"last_run_at": "2026-07-30T22:55:00+09:00",
|
||||
"last_duration": null,
|
||||
"next_run_at": "2026-07-08T12:00:00+09:00",
|
||||
"next_run_at": "2026-08-01T12:00:00+09:00",
|
||||
"creator": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"uuid": "a2640dae-e87c-4a28-b4f1-481fd961dc02",
|
||||
"name": "API 문서 샘플 사용자"
|
||||
},
|
||||
"created_at": "2026-07-08",
|
||||
"created_at": "2026-07-31",
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
"can_update": true,
|
||||
@@ -213,7 +213,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `14` | 기본 키 (내부 식별자) |
|
||||
| id | integer | `12` | 기본 키 (내부 식별자) |
|
||||
| name | string | `실측 예시값` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| description | string | `실측 예시값` | 설명 (다국어 필드는 로케일별 값 객체) |
|
||||
| type | string | `artisan` | 작업 유형: artisan(Artisan 커맨드), shell(쉘 명령), url(URL 호출) |
|
||||
@@ -230,10 +230,10 @@ _단건 응답: `data` 객체의 필드._
|
||||
| last_result_label | string | `미실행` | `last_result` 값의 사람이 읽는 라벨 (현지화/Enum 파생) |
|
||||
| last_duration | null | `null` | 마지막 실행 소요 시간 (초/밀리초 — 실행 이력 파생) |
|
||||
| extension_type | string | `core` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) |
|
||||
| extension_identifier | string | `probe_6a4dc0a862b69` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| creator | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) |
|
||||
| created_at | string | `2026-07-08 12:14:48` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 12:14:48` | 최종 수정 일시 |
|
||||
| extension_identifier | string | `probe_6a71e0dc9a253` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| creator | object | `{"uuid":"a26219fc-94a0-4f63-9404-04c2a6ac99e4","name":"최고…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) |
|
||||
| created_at | string | `2026-08-04 21:53:48` | 생성 일시 |
|
||||
| updated_at | string | `2026-08-04 21:53:48` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -247,7 +247,7 @@ HTTP/1.1 201
|
||||
"success": true,
|
||||
"message": "스케줄이 생성되었습니다.",
|
||||
"data": {
|
||||
"id": 14,
|
||||
"id": 12,
|
||||
"name": "실측 예시값",
|
||||
"description": "실측 예시값",
|
||||
"type": "artisan",
|
||||
@@ -264,13 +264,13 @@ HTTP/1.1 201
|
||||
"last_result_label": "미실행",
|
||||
"last_duration": null,
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "probe_6a4dc0a862b69",
|
||||
"extension_identifier": "probe_6a71e0dc9a253",
|
||||
"creator": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"name": "API 문서 샘플 사용자"
|
||||
"uuid": "a26219fc-94a0-4f63-9404-04c2a6ac99e4",
|
||||
"name": "최고관리자"
|
||||
},
|
||||
"created_at": "2026-07-08 12:14:48",
|
||||
"updated_at": "2026-07-08 12:14:48",
|
||||
"created_at": "2026-08-04 21:53:48",
|
||||
"updated_at": "2026-08-04 21:53:48",
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
"can_update": true,
|
||||
@@ -319,7 +319,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -372,7 +372,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -414,7 +414,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -596,14 +596,14 @@ _단건 응답: `data` 객체의 필드._
|
||||
| is_active | boolean | `true` | active 여부 |
|
||||
| last_result | string | `success` | 마지막 실행 결과: success(성공), failed(실패), running(실행중), never(미실행) |
|
||||
| last_result_label | string | `성공` | `last_result` 값의 사람이 읽는 라벨 (현지화/Enum 파생) |
|
||||
| last_run_at | string | `2026-07-05T19:20:23+09:00` | last run 일시 |
|
||||
| last_run_at | string | `2026-07-30T22:55:00+09:00` | last run 일시 |
|
||||
| last_duration | null | `null` | 마지막 실행의 소요 시간을 사람이 읽는 문자열로 포맷한 값 (예: "45초", "2분 3초" — 마지막 실행 이력의 duration 파생, 실행 이력이 없으면 null) |
|
||||
| next_run_at | string | `2026-07-07T12:00:00+09:00` | next run 일시 |
|
||||
| next_run_at | string | `2026-08-01T12:00:00+09:00` | next run 일시 |
|
||||
| extension_type | null | `null` | 확장 소유 타입: core(코어), module(모듈), plugin(플러그인), NULL(사용자 정의) |
|
||||
| extension_identifier | null | `null` | 확장 식별자 (예: core, sirsoft-board, sirsoft-payment) |
|
||||
| creator | object | `{"uuid":"a231747f-e82e-4cf2-9ae1-a261849dce40","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) |
|
||||
| created_at | string | `2026-07-06 19:20:23` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-06 19:20:23` | 최종 수정 일시 |
|
||||
| creator | object | `{"uuid":"a2640dae-e87c-4a28-b4f1-481fd961dc02","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) |
|
||||
| created_at | string | `2026-07-31 22:55:00` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-31 22:55:00` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -632,17 +632,17 @@ HTTP/1.1 200
|
||||
"is_active": true,
|
||||
"last_result": "success",
|
||||
"last_result_label": "성공",
|
||||
"last_run_at": "2026-07-07T10:41:24+09:00",
|
||||
"last_run_at": "2026-07-30T22:55:00+09:00",
|
||||
"last_duration": null,
|
||||
"next_run_at": "2026-07-08T12:00:00+09:00",
|
||||
"next_run_at": "2026-08-01T12:00:00+09:00",
|
||||
"extension_type": null,
|
||||
"extension_identifier": null,
|
||||
"creator": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"uuid": "a2640dae-e87c-4a28-b4f1-481fd961dc02",
|
||||
"name": "API 문서 샘플 사용자"
|
||||
},
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 10:41:24",
|
||||
"created_at": "2026-07-31 22:55:00",
|
||||
"updated_at": "2026-07-31 22:55:00",
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
"can_update": true,
|
||||
@@ -738,13 +738,13 @@ _단건 응답: `data` 객체의 필드._
|
||||
| is_active | boolean | `true` | active 여부 |
|
||||
| last_result | string | `success` | 마지막 실행 결과: success(성공), failed(실패), running(실행중), never(미실행) |
|
||||
| last_result_label | string | `성공` | `last_result` 값의 사람이 읽는 라벨 (현지화/Enum 파생) |
|
||||
| last_run_at | string | `2026-07-07T10:41:24+09:00` | last run 일시 |
|
||||
| last_run_at | string | `2026-07-30T22:55:00+09:00` | last run 일시 |
|
||||
| last_duration | null | `null` | 마지막 실행 소요 시간 (초/밀리초 — 실행 이력 파생) |
|
||||
| extension_type | string | `core` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) |
|
||||
| extension_identifier | string | `probe_6a4dc0a8e44c0` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| creator | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) |
|
||||
| created_at | string | `2026-07-08 10:41:24` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 12:14:48` | 최종 수정 일시 |
|
||||
| extension_identifier | string | `probe_6a71e0dceb9a2` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| creator | object | `{"uuid":"a2640dae-e87c-4a28-b4f1-481fd961dc02","name":"AP…` | 생성자 정보 객체 (uuid/name/email — creator 관계 파생) |
|
||||
| created_at | string | `2026-07-31 22:55:00` | 생성 일시 |
|
||||
| updated_at | string | `2026-08-04 21:53:48` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -773,16 +773,16 @@ HTTP/1.1 200
|
||||
"is_active": true,
|
||||
"last_result": "success",
|
||||
"last_result_label": "성공",
|
||||
"last_run_at": "2026-07-07T10:41:24+09:00",
|
||||
"last_run_at": "2026-07-30T22:55:00+09:00",
|
||||
"last_duration": null,
|
||||
"extension_type": "core",
|
||||
"extension_identifier": "probe_6a4dc0a8e44c0",
|
||||
"extension_identifier": "probe_6a71e0dceb9a2",
|
||||
"creator": {
|
||||
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"uuid": "a2640dae-e87c-4a28-b4f1-481fd961dc02",
|
||||
"name": "API 문서 샘플 사용자"
|
||||
},
|
||||
"created_at": "2026-07-08 10:41:24",
|
||||
"updated_at": "2026-07-08 12:14:48",
|
||||
"created_at": "2026-07-31 22:55:00",
|
||||
"updated_at": "2026-08-04 21:53:48",
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
"can_update": true,
|
||||
@@ -834,7 +834,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `15` | 기본 키 (내부 식별자) |
|
||||
| id | integer | `13` | 기본 키 (내부 식별자) |
|
||||
| name | string | `API 문서 샘플 스케줄 (복사본)` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
|
||||
| description | string | `문서 실측용 스케줄` | 설명 (다국어 필드는 로케일별 값 객체) |
|
||||
| type | string | `artisan` | 작업 유형: artisan(Artisan 커맨드), shell(쉘 명령), url(URL 호출) |
|
||||
@@ -852,8 +852,8 @@ _단건 응답: `data` 객체의 필드._
|
||||
| last_duration | null | `null` | 마지막 실행 소요 시간 (초/밀리초 — 실행 이력 파생) |
|
||||
| extension_type | null | `null` | 이 리소스를 소유한 확장의 타입 (core/module/plugin/template) |
|
||||
| extension_identifier | null | `null` | 이 리소스를 소유한 확장의 식별자 |
|
||||
| created_at | string | `2026-07-08 12:14:48` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 12:14:48` | 최종 수정 일시 |
|
||||
| created_at | string | `2026-08-04 21:53:48` | 생성 일시 |
|
||||
| updated_at | string | `2026-08-04 21:53:48` | 최종 수정 일시 |
|
||||
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true,"c…` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -867,7 +867,7 @@ HTTP/1.1 201
|
||||
"success": true,
|
||||
"message": "스케줄이 복제되었습니다.",
|
||||
"data": {
|
||||
"id": 15,
|
||||
"id": 13,
|
||||
"name": "API 문서 샘플 스케줄 (복사본)",
|
||||
"description": "문서 실측용 스케줄",
|
||||
"type": "artisan",
|
||||
@@ -885,8 +885,8 @@ HTTP/1.1 201
|
||||
"last_duration": null,
|
||||
"extension_type": null,
|
||||
"extension_identifier": null,
|
||||
"created_at": "2026-07-08 12:14:48",
|
||||
"updated_at": "2026-07-08 12:14:48",
|
||||
"created_at": "2026-08-04 21:53:48",
|
||||
"updated_at": "2026-08-04 21:53:48",
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
"can_update": true,
|
||||
@@ -1013,23 +1013,23 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `7` | 기본 키 (내부 식별자) |
|
||||
| id | integer | `6` | 기본 키 (내부 식별자) |
|
||||
| schedule_id | integer | `1` | schedule 식별자 (연관 리소스 참조) |
|
||||
| started_at | string | `2026-07-08T12:14:49+09:00` | started 일시 |
|
||||
| ended_at | string | `2026-07-08T12:14:49+09:00` | ended 일시 |
|
||||
| started_at | string | `2026-08-04T21:53:49+09:00` | started 일시 |
|
||||
| ended_at | string | `2026-08-04T21:53:49+09:00` | ended 일시 |
|
||||
| duration | integer | `0` | 실행 소요 시간 (초/밀리초) |
|
||||
| duration_formatted | null | `null` | `duration` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) |
|
||||
| status | string | `success` | 상태 값 (도메인별 상태 집합 — 사람이 읽는 라벨은 status_label, UI 변형은 status_variant 참조) |
|
||||
| status_label | string | `성공` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) |
|
||||
| exit_code | integer | `0` | 실행 종료 코드 (0=성공, 그 외=실패) |
|
||||
| memory_usage | integer | `427352` | 실행 중 최대 메모리 사용량 (바이트) |
|
||||
| memory_usage_formatted | string | `417.34 KB` | `memory_usage` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) |
|
||||
| memory_usage | integer | `427312` | 실행 중 최대 메모리 사용량 (바이트) |
|
||||
| memory_usage_formatted | string | `417.3 KB` | `memory_usage` 값의 표시용 포맷 문자열 (통화/용량/일시 등 로케일·단위 포맷) |
|
||||
| output | string | ` INFO Application cache cleared s…` | 실행 표준 출력 내용 |
|
||||
| error_output | null | `null` | 실행 표준 에러 출력 내용 (없으면 빈 문자열/null) |
|
||||
| trigger_type | string | `manual` | 동작을 유발한 방식/주체 구분 값 |
|
||||
| trigger_type_label | string | `수동 실행` | `trigger_type` 값의 사람이 읽는 라벨 (현지화/Enum 파생) |
|
||||
| created_at | string | `2026-07-08 12:14:49` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-08 12:14:49` | 최종 수정 일시 |
|
||||
| created_at | string | `2026-08-04 21:53:49` | 생성 일시 |
|
||||
| updated_at | string | `2026-08-04 21:53:49` | 최종 수정 일시 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1042,23 +1042,23 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "스케줄이 실행되었습니다.",
|
||||
"data": {
|
||||
"id": 7,
|
||||
"id": 6,
|
||||
"schedule_id": 1,
|
||||
"started_at": "2026-07-08T12:14:49+09:00",
|
||||
"ended_at": "2026-07-08T12:14:49+09:00",
|
||||
"started_at": "2026-08-04T21:53:49+09:00",
|
||||
"ended_at": "2026-08-04T21:53:49+09:00",
|
||||
"duration": 0,
|
||||
"duration_formatted": null,
|
||||
"status": "success",
|
||||
"status_label": "성공",
|
||||
"exit_code": 0,
|
||||
"memory_usage": 427352,
|
||||
"memory_usage_formatted": "417.34 KB",
|
||||
"memory_usage": 427312,
|
||||
"memory_usage_formatted": "417.3 KB",
|
||||
"output": "\n INFO Application cache cleared successfully. \n\r\n",
|
||||
"error_output": null,
|
||||
"trigger_type": "manual",
|
||||
"trigger_type_label": "수동 실행",
|
||||
"created_at": "2026-07-08 12:14:49",
|
||||
"updated_at": "2026-07-08 12:14:49"
|
||||
"created_at": "2026-08-04 21:53:49",
|
||||
"updated_at": "2026-08-04 21:53:49"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Search 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -35,8 +35,6 @@
|
||||
| board_slug | query | string | 아니오 | max 100 | 검색 범위를 특정 게시판으로 한정 (게시판 모듈이 `core.search.index_validation_rules` 훅으로 추가하는 파라미터, 해당 slug의 게시판 글만 검색) |
|
||||
| category_id | query | integer | 아니오 | — | category 식별자 |
|
||||
|
||||
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.search.index_validation_rules`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
|
||||
+18
-27
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Seo 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -106,7 +106,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -148,12 +148,12 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드 — `GET /api/admin/seo/sitemap/status` 와 동일한 스키마입니다._
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| last_updated_at | string\|null | `2026-07-08T03:14:49+00:00` | **예약 시점 기준** 마지막 생성 일시. 이번 재생성의 결과가 아니라 직전 생성 시각이며, 잡 완료 후 갱신됩니다 (아직 한 번도 생성되지 않았으면 null) |
|
||||
| progress | object\|null | `{"status":"queued","mode":"full",...}` | 재생성 진행상황. 예약 직후 `status=queued`, `mode=full` 로 즉시 기록됩니다. 필드 상세는 아래 상태 조회 API 참조 (진행 이력이 없으면 null) |
|
||||
| last_updated_at | string | `2026-08-04T02:29:57+00:00` | **예약 시점 기준** 마지막 생성 일시. 이번 재생성의 결과가 아니라 직전 생성 시각이며, 잡 완료 후 갱신됩니다 (아직 한 번도 생성되지 않았으면 null) |
|
||||
| progress | object | `{"status":"queued","mode":"full","started_at":"2026-08-04…` | 재생성 진행상황. 예약 직후 `status=queued`, `mode=full` 로 즉시 기록됩니다. 필드 상세는 아래 상태 조회 API 참조 (진행 이력이 없으면 null) |
|
||||
| realtime_enabled | boolean | `false` | 실시간 연결(WebSocket) 가능 여부. true 면 프론트가 진행상황 채널을 구독하고, false 면 상태 API 를 주기적으로 폴링합니다 |
|
||||
|
||||
**응답 예시**
|
||||
@@ -167,11 +167,11 @@ HTTP/1.1 200
|
||||
"success": true,
|
||||
"message": "Sitemap 재생성을 시작했습니다. 완료까지 시간이 걸릴 수 있습니다.",
|
||||
"data": {
|
||||
"last_updated_at": "2026-07-08T03:14:49+00:00",
|
||||
"last_updated_at": "2026-08-04T02:29:57+00:00",
|
||||
"progress": {
|
||||
"status": "queued",
|
||||
"mode": "full",
|
||||
"started_at": "2026-07-08T05:20:00+00:00",
|
||||
"started_at": "2026-08-04T12:53:51+00:00",
|
||||
"finished_at": null,
|
||||
"phase": null,
|
||||
"urls": 0,
|
||||
@@ -188,7 +188,6 @@ HTTP/1.1 200
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 400 | Bad Request | SEO 설정에서 sitemap 생성이 비활성(`seo.sitemap_enabled=false`)인 경우 (`seo.sitemap_disabled`) |
|
||||
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 |
|
||||
|
||||
@@ -204,6 +203,7 @@ sitemap.xml 재생성을 큐에 예약합니다. 관리자 수동 재생성은
|
||||
|
||||
이 엔드포인트로 접수된 **관리자 수동 재생성**은 실행한 관리자의 ID 를 잡에 실어, 생성 완료 시 `sitemap_regenerated`, 실패 시 `sitemap_regenerate_failed` 알림이 **그 관리자에게만** 발송됩니다(기본 채널: 앱 내 알림). 스케줄러·리소스 변경(증분)·봇 캐시 미스로 유발된 재생성은 실행 관리자가 없어 알림을 보내지 않습니다. 알림 정의는 `config/core.php` 의 `notification_definitions` 에 있으며 관리자 알림 설정에서 채널을 조정할 수 있습니다.
|
||||
|
||||
|
||||
### GET /api/admin/seo/sitemap/status
|
||||
<!-- @generated:start:api.admin.seo.sitemap.status -->
|
||||
- **라우트명**: `api.admin.seo.sitemap.status`
|
||||
@@ -229,17 +229,8 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| last_updated_at | string\|null | `2026-07-08T03:14:49+00:00` | 마지막 생성 일시 (아직 한 번도 생성되지 않았으면 null) |
|
||||
| progress | object\|null | `{"status":"running",...}` | 재생성 진행상황. 진행 이력이 없으면 null |
|
||||
| progress.status | string | `running` | `queued`(예약) / `running`(기여자 처리 중) / `writing`(파일 작성 중) / `completed`(완료) / `failed`(실패) |
|
||||
| progress.mode | string\|null | `full` | 재생성 모드 (`full` / `auto` / `incremental`) |
|
||||
| progress.phase | string\|null | `sirsoft-board` | 현재 처리 중인 기여자 식별자 (running 단계에서만) |
|
||||
| progress.urls | integer | `12000` | 누적 처리 URL 수 |
|
||||
| progress.url_count | integer\|null | `5000` | 완료 시 총 URL 수 |
|
||||
| progress.child_count | integer\|null | `2` | 완료 시 분할 파일 수 |
|
||||
| progress.started_at | string\|null | `2026-07-08T05:20:00+00:00` | 예약 시각 |
|
||||
| progress.finished_at | string\|null | `2026-07-08T05:21:30+00:00` | 완료/실패 시각 |
|
||||
| progress.message | string\|null | `null` | 실패 시 오류 메시지 |
|
||||
| last_updated_at | string | `2026-08-04T12:53:51+00:00` | 마지막 생성 일시 (아직 한 번도 생성되지 않았으면 null) |
|
||||
| progress | object | `{"status":"queued","mode":"full","started_at":"2026-08-04…` | 재생성 진행상황. 진행 이력이 없으면 null |
|
||||
| realtime_enabled | boolean | `false` | 실시간 연결(WebSocket) 가능 여부. true 면 프론트가 `core.admin.seo.sitemap` 채널을 구독하고, false 면 이 API 를 주기적으로 폴링합니다 |
|
||||
|
||||
**응답 예시**
|
||||
@@ -251,16 +242,16 @@ HTTP/1.1 200
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "성공적으로 처리되었습니다.",
|
||||
"message": "messages.success",
|
||||
"data": {
|
||||
"last_updated_at": "2026-07-08T03:14:49+00:00",
|
||||
"last_updated_at": "2026-08-04T12:53:51+00:00",
|
||||
"progress": {
|
||||
"status": "running",
|
||||
"status": "queued",
|
||||
"mode": "full",
|
||||
"started_at": "2026-07-08T05:20:00+00:00",
|
||||
"started_at": "2026-08-04T12:53:51+00:00",
|
||||
"finished_at": null,
|
||||
"phase": "sirsoft-board",
|
||||
"urls": 12000,
|
||||
"phase": null,
|
||||
"urls": 0,
|
||||
"url_count": null,
|
||||
"child_count": null,
|
||||
"message": null
|
||||
@@ -374,7 +365,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
+59
-358
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Settings 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -42,20 +42,21 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| general | object | `{"site_name":"Test Site","site_url":"https:\/\/test.examp…` | 일반 탭 설정 그룹 (사이트명·사이트 URL·설명·관리자 이메일·타임존·기본 언어·통화·점검 모드·사이트 로고 첨부). site_logo 는 SettingsService 가 별도 주입한 첨부 정보 |
|
||||
| security | object | `{"force_https":true,"login_attempt_enabled":true,"auth_to…` | 보안 탭 설정 그룹 (HTTPS 강제·로그인 시도 제한 사용·인증 토큰 유지시간(분, 0=무한)·최대 로그인 시도 횟수·잠금 시간·내부 네트워크 주소 호출 허용). `allow_internal_outbound_urls`(boolean, 기본 false): 서버가 대신 호출하는 outbound 요청(예약 작업 URL 호출, 외부 API 연동)에서 사설 IP·`localhost` 등 내부 주소를 허용할지 여부 — 켜면 사내 서버 호출이 가능해지지만 서버가 내부망으로 요청을 보낼 수 있게 되므로 기본은 차단입니다. 언어팩 URL 설치는 원격 코드를 내려받으므로 이 설정과 무관하게 항상 내부 주소를 거부합니다 |
|
||||
| general | object | `{"site_name":"그누보드7","site_url":"https:\/\/g7_2.dev","sit…` | 일반 탭 설정 그룹 (사이트명·사이트 URL·설명·관리자 이메일·타임존·기본 언어·통화·점검 모드·사이트 로고 첨부). site_logo 는 SettingsService 가 별도 주입한 첨부 정보 |
|
||||
| security | object | `{"force_https":false,"login_attempt_enabled":true,"auth_t…` | 보안 탭 설정 그룹 (HTTPS 강제·로그인 시도 제한 사용·인증 토큰 유지시간(분, 0=무한)·최대 로그인 시도 횟수·잠금 시간·내부 네트워크 주소 호출 허용). `allow_internal_outbound_urls`(boolean, 기본 false): 서버가 대신 호출하는 outbound 요청(예약 작업 URL 호출, 외부 API 연동)에서 사설 IP·`localhost` 등 내부 주소를 허용할지 여부 — 켜면 사내 서버 호출이 가능해지지만 서버가 내부망으로 요청을 보낼 수 있게 되므로 기본은 차단입니다. 언어팩 URL 설치는 원격 코드를 내려받으므로 이 설정과 무관하게 항상 내부 주소를 거부합니다 |
|
||||
| mail | object | `{"mailer":"smtp","host":"","port":587,"username":"","pass…` | 메일 탭 설정 그룹 (메일러 종류(smtp/mailgun/ses)·SMTP 호스트/포트/인증 정보·암호화 방식·발신자 주소/이름·Mailgun/SES 자격 정보) |
|
||||
| upload | object | `{"max_file_size":10,"allowed_extensions":["jpg","jpeg","p…` | 업로드 탭 설정 그룹 (최대 파일 크기(MB)·허용 확장자 목록·이미지 최대 가로/세로·이미지 품질) |
|
||||
| seo | object | `{"meta_title_suffix":"","meta_description":"","meta_keywo…` | SEO 탭 설정 그룹 (메타 타이틀 접미사·메타 설명/키워드·검색엔진 인증 코드·봇 감지·OG/Twitter 기본값·SEO 캐시·사이트맵·생성기 설정) |
|
||||
| seo | object | `{"meta_title_suffix":null,"meta_description":null,"meta_k…` | SEO 탭 설정 그룹 (메타 타이틀 접미사·메타 설명/키워드·검색엔진 인증 코드·봇 감지·OG/Twitter 기본값·SEO 캐시·사이트맵·생성기 설정) |
|
||||
| advanced | object | `{"cache_enabled":true,"cache_default_ttl":86400,"layout_c…` | 고급 탭 설정 그룹 (캐시·디버그·코어 업데이트·GeoIP 설정을 한 탭으로 합친 병합 뷰). cache/debug 카테고리 값이 함께 노출됨 |
|
||||
| cache | object | `{"cache_enabled":true,"cache_default_ttl":86400,"layout_c…` | 캐시 원본 카테고리 (전역 캐시 사용·기본 TTL·레이아웃/통계/SEO 캐시 사용 및 TTL). advanced 탭에 병합되면서 개별 접근용으로 별도 노출된 파생 뷰 |
|
||||
| debug | object | `{"debug_mode":true,"sql_query_log":false,"log_level":"err…` | 디버그 원본 카테고리 (디버그 모드·SQL 쿼리 로그·로그 레벨). advanced 탭에 병합되면서 개별 접근용으로 별도 노출된 파생 뷰 |
|
||||
| debug | object | `{"debug_mode":false,"sql_query_log":false,"log_level":"er…` | 디버그 원본 카테고리 (디버그 모드·SQL 쿼리 로그·로그 레벨). advanced 탭에 병합되면서 개별 접근용으로 별도 노출된 파생 뷰 |
|
||||
| drivers | object | `{"storage_driver":"local","s3_bucket":null,"s3_region":"a…` | 드라이버 탭 설정 그룹 (스토리지/캐시/세션/큐/로그 드라이버 선택 + S3·Redis·Memcached·WebSocket·검색엔진 접속 파라미터) |
|
||||
| core_update | object | `{"core_update_github_url":"https:\/\/github.com\/custom\/…` | 코어 업데이트 원본 카테고리 (코어 업데이트를 받아올 GitHub 저장소 URL·비공개 저장소 접근용 토큰). advanced 탭에 병합된 파생 뷰 |
|
||||
| core_update | object | `{"core_update_github_url":"https:\/\/github.com\/gnuboard…` | 코어 업데이트 원본 카테고리 (코어 업데이트를 받아올 GitHub 저장소 URL·비공개 저장소 접근용 토큰). advanced 탭에 병합된 파생 뷰 |
|
||||
| geoip | object | `{"geoip_enabled":false,"geoip_license_key":null,"geoip_au…` | GeoIP 원본 카테고리 (GeoIP 사용 여부·MaxMind 라이선스 키·DB 자동 갱신 사용). advanced 탭에 병합된 파생 뷰 |
|
||||
| notifications | object | `{"channels":[{"id":"mail","is_active":true,"sort_order":1…` | 알림 탭 설정 그룹. channels 는 알림 채널 목록으로 각 원소가 id(채널 식별자)·is_active(활성 여부)·sort_order(표시 순서)를 가짐 |
|
||||
| identity | object | `{"default_provider":"g7:core.mail","purpose_providers":{"…` | 본인인증(IDV) 탭 설정 그룹 (기본 provider·목적별 provider 매핑(purpose_providers)·챌린지 유효시간(분)·최대 시도 횟수) |
|
||||
| available_drivers | object | `{"storage":[{"id":"local","label":{"ko":"로컬","en":"Local"…` | 드라이버 선택지 카탈로그 (DriverRegistryService 산물). 종류별(storage/cache/session/queue 등) 선택 가능한 드라이버 목록을 id/다국어 label 형태로 제공 |
|
||||
| _meta | object | `{"limits":{"upload_max_file_size_min":1,"upload_max_file_…` | <!-- TODO: 설명 --> |
|
||||
| abilities | object | `{"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
@@ -70,25 +71,20 @@ HTTP/1.1 200
|
||||
"message": "설정을 성공적으로 가져왔습니다.",
|
||||
"data": {
|
||||
"general": {
|
||||
"site_name": "Test Site",
|
||||
"site_url": "https://test.example.com",
|
||||
"site_description": "",
|
||||
"admin_email": "admin@example.com",
|
||||
"site_name": "그누보드7",
|
||||
"site_url": "https://api.example.com",
|
||||
"site_description": null,
|
||||
"admin_email": "heuristing@gmail.com",
|
||||
"timezone": "Asia/Seoul",
|
||||
"language": "ko",
|
||||
"currency": "KRW",
|
||||
"maintenance_mode": false,
|
||||
"site_logo": []
|
||||
"...": "(5개 키 생략, 총 10개)"
|
||||
},
|
||||
"security": {
|
||||
"force_https": true,
|
||||
"force_https": false,
|
||||
"login_attempt_enabled": true,
|
||||
"auth_token_lifetime": "{MASKED}",
|
||||
"max_login_attempts": 5,
|
||||
"login_lockout_time": 30,
|
||||
"two_factor_auth": false,
|
||||
"password_min_length": "{MASKED}",
|
||||
"require_password_special_char": "{MASKED}"
|
||||
"login_lockout_time": 5,
|
||||
"...": "(4개 키 생략, 총 9개)"
|
||||
},
|
||||
"mail": {
|
||||
"mailer": "smtp",
|
||||
@@ -96,15 +92,7 @@ HTTP/1.1 200
|
||||
"port": 587,
|
||||
"username": "",
|
||||
"password": "{MASKED}",
|
||||
"encryption": "tls",
|
||||
"mailgun_domain": "",
|
||||
"mailgun_secret": "{MASKED}",
|
||||
"mailgun_endpoint": "api.mailgun.net",
|
||||
"ses_key": "",
|
||||
"ses_secret": "{MASKED}",
|
||||
"ses_region": "ap-northeast-2",
|
||||
"from_address": "heuristing@gmail.com",
|
||||
"from_name": "그누보드7"
|
||||
"...": "(9개 키 생략, 총 14개)"
|
||||
},
|
||||
"upload": {
|
||||
"max_file_size": 10,
|
||||
@@ -114,313 +102,21 @@ HTTP/1.1 200
|
||||
"png",
|
||||
"gif",
|
||||
"webp",
|
||||
"pdf",
|
||||
"doc",
|
||||
"docx",
|
||||
"xls",
|
||||
"xlsx",
|
||||
"zip"
|
||||
"... (총 11건 중 5건 표시)"
|
||||
],
|
||||
"image_max_width": 2000,
|
||||
"image_max_height": 2000,
|
||||
"image_max_width": "800",
|
||||
"image_max_height": "600",
|
||||
"image_quality": 85
|
||||
},
|
||||
"seo": {
|
||||
"meta_title_suffix": "",
|
||||
"meta_description": "",
|
||||
"meta_keywords": "",
|
||||
"google_analytics_id": "",
|
||||
"google_site_verification": "",
|
||||
"naver_site_verification": "",
|
||||
"bot_user_agents": [
|
||||
"kakaotalk-scrap",
|
||||
"Meta-ExternalAgent",
|
||||
"ChatGPT-User"
|
||||
],
|
||||
"bot_detection_enabled": true,
|
||||
"bot_detection_library_enabled": true,
|
||||
"og_default_site_name": "",
|
||||
"og_image_default_width": 1200,
|
||||
"og_image_default_height": 630,
|
||||
"twitter_default_card": "summary_large_image",
|
||||
"twitter_default_site": "",
|
||||
"cache_enabled": true,
|
||||
"cache_ttl": 7200,
|
||||
"sitemap_enabled": true,
|
||||
"sitemap_cache_ttl": 86400,
|
||||
"sitemap_urls_per_file": 50000,
|
||||
"sitemap_gzip": false,
|
||||
"sitemap_serve_stale_on_miss": true,
|
||||
"sitemap_max_urls_per_contributor": 0,
|
||||
"sitemap_hreflang_enabled": true,
|
||||
"sitemap_schedule": "daily",
|
||||
"sitemap_schedule_time": "02:00",
|
||||
"sitemap_last_updated_at": "2026-07-08T03:14:49+00:00",
|
||||
"generator_enabled": true,
|
||||
"generator_content": ""
|
||||
"meta_title_suffix": null,
|
||||
"meta_description": null,
|
||||
"meta_keywords": null,
|
||||
"google_analytics_id": null,
|
||||
"google_site_verification": null,
|
||||
"...": "(24개 키 생략, 총 29개)"
|
||||
},
|
||||
"advanced": {
|
||||
"cache_enabled": true,
|
||||
"cache_default_ttl": 86400,
|
||||
"layout_cache_enabled": true,
|
||||
"layout_cache_ttl": 3600,
|
||||
"seo_cache_enabled": true,
|
||||
"seo_cache_ttl": 7200,
|
||||
"seo_sitemap_cache_ttl": 86400,
|
||||
"stats_cache_enabled": true,
|
||||
"stats_cache_ttl": 1800,
|
||||
"notification_cache_ttl": 3600,
|
||||
"extension_status_cache_ttl": 86400,
|
||||
"geoip_cache_enabled": true,
|
||||
"geoip_cache_ttl": 86400,
|
||||
"version_check_cache_ttl": 3600,
|
||||
"debug_mode": true,
|
||||
"sql_query_log": false,
|
||||
"log_level": "error",
|
||||
"core_update_github_url": "https://github.com/custom/repo",
|
||||
"core_update_github_token": "{MASKED}",
|
||||
"geoip_enabled": false,
|
||||
"geoip_license_key": null,
|
||||
"geoip_auto_update_enabled": true,
|
||||
"geoip_last_updated_at": ""
|
||||
},
|
||||
"cache": {
|
||||
"cache_enabled": true,
|
||||
"cache_default_ttl": 86400,
|
||||
"layout_cache_enabled": true,
|
||||
"layout_cache_ttl": 3600,
|
||||
"seo_cache_enabled": true,
|
||||
"seo_cache_ttl": 7200,
|
||||
"seo_sitemap_cache_ttl": 86400,
|
||||
"stats_cache_enabled": true,
|
||||
"stats_cache_ttl": 1800,
|
||||
"notification_cache_ttl": 3600,
|
||||
"extension_status_cache_ttl": 86400,
|
||||
"geoip_cache_enabled": true,
|
||||
"geoip_cache_ttl": 86400,
|
||||
"version_check_cache_ttl": 3600
|
||||
},
|
||||
"debug": {
|
||||
"debug_mode": true,
|
||||
"sql_query_log": false,
|
||||
"log_level": "error"
|
||||
},
|
||||
"drivers": {
|
||||
"storage_driver": "local",
|
||||
"s3_bucket": null,
|
||||
"s3_region": "ap-northeast-2",
|
||||
"s3_access_key": null,
|
||||
"s3_secret_key": null,
|
||||
"s3_url": null,
|
||||
"cache_driver": "file",
|
||||
"redis_host": "127.0.0.1",
|
||||
"redis_port": 6379,
|
||||
"redis_password": null,
|
||||
"redis_database": "1",
|
||||
"memcached_host": "127.0.0.1",
|
||||
"memcached_port": 11211,
|
||||
"session_driver": "file",
|
||||
"session_lifetime": 120,
|
||||
"queue_driver": "sync",
|
||||
"log_driver": "daily",
|
||||
"log_level": "error",
|
||||
"log_days": 14,
|
||||
"websocket_enabled": true,
|
||||
"websocket_app_id": "test-app-id",
|
||||
"websocket_app_key": "{MASKED}",
|
||||
"websocket_app_secret": "{MASKED}",
|
||||
"websocket_host": "localhost",
|
||||
"websocket_port": 8080,
|
||||
"websocket_scheme": "https",
|
||||
"websocket_verify_ssl": true,
|
||||
"websocket_server_host": "127.0.0.1",
|
||||
"websocket_server_port": 8080,
|
||||
"websocket_server_scheme": "http",
|
||||
"search_engine_driver": "mysql-fulltext"
|
||||
},
|
||||
"core_update": {
|
||||
"core_update_github_url": "https://github.com/custom/repo",
|
||||
"core_update_github_token": "{MASKED}"
|
||||
},
|
||||
"geoip": {
|
||||
"geoip_enabled": false,
|
||||
"geoip_license_key": null,
|
||||
"geoip_auto_update_enabled": true,
|
||||
"geoip_last_updated_at": ""
|
||||
},
|
||||
"notifications": {
|
||||
"channels": [
|
||||
{
|
||||
"id": "mail",
|
||||
"is_active": true,
|
||||
"sort_order": 1
|
||||
},
|
||||
{
|
||||
"id": "database",
|
||||
"is_active": true,
|
||||
"sort_order": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"identity": {
|
||||
"default_provider": "g7:core.mail",
|
||||
"purpose_providers": {
|
||||
"signup": null,
|
||||
"password_reset": null,
|
||||
"inicis": {
|
||||
"adult_verification": "inicis"
|
||||
}
|
||||
},
|
||||
"challenge_ttl_minutes": 15,
|
||||
"max_attempts": 5
|
||||
},
|
||||
"available_drivers": {
|
||||
"storage": [
|
||||
{
|
||||
"id": "local",
|
||||
"label": {
|
||||
"ko": "로컬",
|
||||
"en": "Local",
|
||||
"fr": "로컬"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "s3",
|
||||
"label": {
|
||||
"ko": "Amazon S3",
|
||||
"en": "Amazon S3",
|
||||
"fr": "Amazon S3"
|
||||
}
|
||||
}
|
||||
],
|
||||
"cache": [
|
||||
{
|
||||
"id": "file",
|
||||
"label": {
|
||||
"ko": "파일",
|
||||
"en": "File",
|
||||
"fr": "파일"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "redis",
|
||||
"label": {
|
||||
"ko": "Redis",
|
||||
"en": "Redis",
|
||||
"fr": "Redis"
|
||||
}
|
||||
}
|
||||
],
|
||||
"session": [
|
||||
{
|
||||
"id": "file",
|
||||
"label": {
|
||||
"ko": "파일",
|
||||
"en": "File",
|
||||
"fr": "파일"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "database",
|
||||
"label": {
|
||||
"ko": "데이터베이스",
|
||||
"en": "Database",
|
||||
"fr": "데이터베이스"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "redis",
|
||||
"label": {
|
||||
"ko": "Redis",
|
||||
"en": "Redis",
|
||||
"fr": "Redis"
|
||||
}
|
||||
}
|
||||
],
|
||||
"queue": [
|
||||
{
|
||||
"id": "sync",
|
||||
"label": {
|
||||
"ko": "동기",
|
||||
"en": "Sync",
|
||||
"fr": "동기"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "database",
|
||||
"label": {
|
||||
"ko": "데이터베이스",
|
||||
"en": "Database",
|
||||
"fr": "데이터베이스"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "redis",
|
||||
"label": {
|
||||
"ko": "Redis",
|
||||
"en": "Redis",
|
||||
"fr": "Redis"
|
||||
}
|
||||
}
|
||||
],
|
||||
"log": [
|
||||
{
|
||||
"id": "single",
|
||||
"label": {
|
||||
"ko": "단일 파일",
|
||||
"en": "Single File",
|
||||
"fr": "단일 파일"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "daily",
|
||||
"label": {
|
||||
"ko": "일별 파일",
|
||||
"en": "Daily File",
|
||||
"fr": "일별 파일"
|
||||
}
|
||||
}
|
||||
],
|
||||
"websocket": [
|
||||
{
|
||||
"id": "reverb",
|
||||
"label": {
|
||||
"ko": "Laravel Reverb",
|
||||
"en": "Laravel Reverb",
|
||||
"fr": "Laravel Reverb"
|
||||
}
|
||||
}
|
||||
],
|
||||
"mail": [
|
||||
{
|
||||
"id": "smtp",
|
||||
"label": {
|
||||
"ko": "SMTP",
|
||||
"en": "SMTP",
|
||||
"fr": "SMTP"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mailgun",
|
||||
"label": {
|
||||
"ko": "Mailgun",
|
||||
"en": "Mailgun",
|
||||
"fr": "Mailgun"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ses",
|
||||
"label": {
|
||||
"ko": "SES (Amazon)",
|
||||
"en": "SES (Amazon)",
|
||||
"fr": "SES (Amazon)"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"abilities": {
|
||||
"can_update": true
|
||||
}
|
||||
"...": "(11개 키 생략, 총 16개)"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -505,7 +201,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -551,7 +247,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| app_key | string | `base64:97gZH*************************…` | 현재 애플리케이션 키(`APP_KEY`)를 마스킹한 문자열. 앞부분 일부만 노출하고 나머지는 별표로 가려 전체 원문은 반환하지 않음 |
|
||||
| app_key | string | `base64:YlAis*************************…` | 현재 애플리케이션 키(`APP_KEY`)를 마스킹한 문자열. 앞부분 일부만 노출하고 나머지는 별표로 가려 전체 원문은 반환하지 않음 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -604,7 +300,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -645,7 +341,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -686,7 +382,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -727,7 +423,7 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -830,7 +526,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -881,7 +577,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -928,19 +624,20 @@ _단건 응답: `data` 객체의 필드._
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| os_info | string | `Windows NT 10.0` | 운영체제 종류와 버전 (`php_uname` 산물). probe 차단 시 "알 수 없음" 폴백 |
|
||||
| web_server | string | `Apache/2.4.62 (Win64) OpenSSL/3.0.16 …` | 웹서버 소프트웨어 식별 문자열 (`$_SERVER['SERVER_SOFTWARE']`) |
|
||||
| web_server | string | `nginx/1.27.3` | 웹서버 소프트웨어 식별 문자열 (`$_SERVER['SERVER_SOFTWARE']`) |
|
||||
| php_version | string | `8.3.26` | 실행 중인 PHP 버전 (`PHP_VERSION`) |
|
||||
| mysql_version | string | `Mysql 8.4.3` | 연결된 데이터베이스 서버 종류와 버전 (DB 조회 산물). probe 실패 시 "알 수 없음" 폴백 |
|
||||
| g7_version | string | `7.0.1` | G7 코어 버전 (`config('app.version')`) |
|
||||
| g7_version | string | `7.0.6` | G7 코어 버전 (`config('app.version')`) |
|
||||
| g7_release_year | string | `2026` | G7 릴리즈 연도 (`config('app.release_year')`, 저작권 표기 등에 사용) |
|
||||
| laravel_version | string | `12.54.1` | 프레임워크 Laravel 버전 (`app()->version()`) |
|
||||
| laravel_version | string | `12.62.0` | 프레임워크 Laravel 버전 (`app()->version()`) |
|
||||
| environment | string | `local` | 현재 실행 환경 (`app()->environment()` — local/production/testing 등) |
|
||||
| cpu_info | string | `Intel(R) Core(TM) Ultra 5 225H` | CPU 모델명 (OS별 시스템 probe 산물). 수집 실패 시 "알 수 없음" 폴백 |
|
||||
| memory_usage | object | `{"total":"31.49 GB","used":"29.63 GB","free":"1.86 GB","p…` | 물리 메모리 사용량. total/used/free 는 사람이 읽기 쉬운 단위 문자열, percentage 는 사용률(%) |
|
||||
| disk_usage | object | `{"total":"474.72 GB","used":"360.2 GB","free":"114.51 GB"…` | 설치 볼륨 디스크 사용량. total/used/free 단위 문자열 + percentage 사용률(%) |
|
||||
| memory_usage | object | `{"total":"31.49 GB","used":"24.31 GB","free":"7.18 GB","p…` | 물리 메모리 사용량. total/used/free 는 사람이 읽기 쉬운 단위 문자열, percentage 는 사용률(%) |
|
||||
| disk_usage | object | `{"total":"474.72 GB","used":"408.15 GB","free":"66.57 GB"…` | 설치 볼륨 디스크 사용량. total/used/free 단위 문자열 + percentage 사용률(%) |
|
||||
| php_memory_limit | string | `512M` | PHP `memory_limit` ini 값 |
|
||||
| max_execution_time | string | `36000초` | PHP `max_execution_time` ini 값 (초 단위 접미사 부착) |
|
||||
| upload_max_filesize | string | `2G` | PHP `upload_max_filesize` ini 값 |
|
||||
| opcache | object | `{"loaded":true,"enabled":true}` | <!-- TODO: 설명 --> |
|
||||
| install_path | string | `C:\Users\HeuJung\htdocs\g7_2` | 애플리케이션 설치 루트 경로 (`base_path()`) |
|
||||
| config_path | string | `C:\Users\HeuJung\htdocs\g7_2\storage\…` | 설정 파일 저장 경로 (`storage/app/settings`) |
|
||||
| log_path | string | `C:\Users\HeuJung\htdocs\g7_2\storage\…` | 로그 파일 저장 경로 (`storage/logs`) |
|
||||
@@ -948,7 +645,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
| php_extensions | object | `{"required":{"openssl":true,"pdo":true,"mbstring":true,"t…` | PHP 확장 로드 상태. required(필수)·optional(선택) 두 그룹으로 나뉘며 각 확장명→로드 여부(bool) 매핑 |
|
||||
| database_config | object | `{"has_read_write_split":false,"write":{"host":"localhost"…` | DB 연결 구성 요약. has_read_write_split(읽기/쓰기 분리 여부)·write(쓰기 연결 정보)·read(읽기 replica 목록, write 와 동일하면 제외) |
|
||||
| timezone | string | `UTC` | 애플리케이션 기본 타임존 (`config('app.timezone')`) |
|
||||
| server_time | string | `2026-07-07 05:08:58` | 서버 현재 시각 (Y-m-d H:i:s) |
|
||||
| server_time | string | `2026-08-04 12:53:55` | 서버 현재 시각 (Y-m-d H:i:s) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -962,29 +659,33 @@ HTTP/1.1 200
|
||||
"message": "성공적으로 처리되었습니다.",
|
||||
"data": {
|
||||
"os_info": "Windows NT 10.0",
|
||||
"web_server": "Apache/2.4.62 (Win64) OpenSSL/3.0.16 PHP/8.3.26",
|
||||
"web_server": "nginx/1.27.3",
|
||||
"php_version": "8.3.26",
|
||||
"mysql_version": "Mysql 8.4.3",
|
||||
"g7_version": "7.0.1",
|
||||
"g7_version": "7.0.6",
|
||||
"g7_release_year": "2026",
|
||||
"laravel_version": "12.54.1",
|
||||
"laravel_version": "12.62.0",
|
||||
"environment": "local",
|
||||
"cpu_info": "Intel(R) Core(TM) Ultra 5 225H",
|
||||
"memory_usage": {
|
||||
"total": "31.49 GB",
|
||||
"used": "26.92 GB",
|
||||
"free": "4.57 GB",
|
||||
"percentage": 85.49
|
||||
"used": "24.31 GB",
|
||||
"free": "7.18 GB",
|
||||
"percentage": 77.2
|
||||
},
|
||||
"disk_usage": {
|
||||
"total": "474.72 GB",
|
||||
"used": "362.59 GB",
|
||||
"free": "112.12 GB",
|
||||
"percentage": 76.38
|
||||
"used": "408.15 GB",
|
||||
"free": "66.57 GB",
|
||||
"percentage": 85.98
|
||||
},
|
||||
"php_memory_limit": "512M",
|
||||
"max_execution_time": "36000초",
|
||||
"upload_max_filesize": "2G",
|
||||
"opcache": {
|
||||
"loaded": true,
|
||||
"enabled": true
|
||||
},
|
||||
"install_path": "C:\\Users\\HeuJung\\htdocs\\g7_2",
|
||||
"config_path": "C:\\Users\\HeuJung\\htdocs\\g7_2\\storage\\app/settings",
|
||||
"log_path": "C:\\Users\\HeuJung\\htdocs\\g7_2\\storage\\logs",
|
||||
@@ -1025,7 +726,7 @@ HTTP/1.1 200
|
||||
"read": []
|
||||
},
|
||||
"timezone": "UTC",
|
||||
"server_time": "2026-07-08 03:14:54"
|
||||
"server_time": "2026-08-04 12:53:55"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1112,7 +813,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1191,7 +892,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-500 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1287,7 +988,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# System API 레퍼런스
|
||||
|
||||
> **소유**: 코어 · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR (5초 요약)
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 System 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
### GET /api/system/asset-probe
|
||||
<!-- @generated:start:api.public.system.asset-probe.extensionless -->
|
||||
- **라우트명**: `api.public.system.asset-probe.extensionless`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Public\AssetProbeController@probe`
|
||||
- **인증/권한**: 공개 (인증 불필요)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/system/asset-probe HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: http-200 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-200 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 -->
|
||||
|
||||
|
||||
### GET /api/system/asset-probe.js
|
||||
<!-- @generated:start:api.public.system.asset-probe -->
|
||||
- **라우트명**: `api.public.system.asset-probe`
|
||||
- **컨트롤러**: `App\Http\Controllers\Api\Public\AssetProbeController@probe`
|
||||
- **인증/권한**: 공개 (인증 불필요)
|
||||
|
||||
**요청 파라미터**
|
||||
|
||||
_요청 파라미터 없음._
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/system/asset-probe.js HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
|
||||
|
||||
<!-- @generated:end -->
|
||||
|
||||
**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 -->
|
||||
|
||||
+1538
-133
File diff suppressed because it is too large
Load Diff
+396
-346
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Verify Password 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -29,8 +29,6 @@
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| password | body | string | 예 | — | 비밀번호 |
|
||||
|
||||
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.auth.verify_password_validation_rules`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
@@ -47,7 +45,7 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: write-method — 응답 필드는 사람이 작성하세요. -->
|
||||
<!-- 실측 제외: http-401 — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
@@ -832,6 +832,43 @@ public function getAll(): Collection
|
||||
}
|
||||
```
|
||||
|
||||
### 목록의 eager load 는 목록이 직렬화하는 관계만
|
||||
|
||||
eager load 는 N+1 을 막지만, **목록에 필요 없는 관계까지 로드하면** 이번에는 페이로드가 커진다. 한 페이지를 여는 것만으로 그 페이지 전 행의 하위 컬렉션이 통째로 메모리에 올라오고 응답에 실린다.
|
||||
|
||||
```php
|
||||
// ❌ Resource 는 whenLoaded 로 방어하는데 Repository 가 목록에서 무조건 로드
|
||||
// → 가드가 항상 참이 되어 무력화된다
|
||||
$listRelations = ['categories', 'images', 'brand', 'options'];
|
||||
|
||||
// ✅ 목록이 실제로 직렬화하는 관계만. 개수·합계는 집계로
|
||||
$listRelations = ['categories', 'images', 'brand'];
|
||||
// withCount: ['options as options_count'], outerUsing: fn ($q) => $q->withSum(...)
|
||||
```
|
||||
|
||||
로드 여부를 정하는 곳은 Repository 다. Resource 의 `whenLoaded` 는 그 결정을 **따르는** 장치이지, 그 자체가 페이로드를 줄이지는 않는다.
|
||||
|
||||
목록에서 뺀 관계는 단건 조회가 공급해야 한다. 이때 **단건이 목록용 조회 메서드를 재사용하지 않도록** 한다 — 목록 조회는 컬럼을 좁히고 건수 상한을 두므로, 재사용하면 단건이 그 제약을 그대로 물려받아 값이 비거나 상한 밖 행을 찾지 못한다.
|
||||
|
||||
```php
|
||||
// ❌ 단건이 목록용 조회를 재사용 → content 소실 + 상한 밖 행은 못 찾음
|
||||
$version = $this->repository->getVersions($id)->firstWhere('version', $version);
|
||||
|
||||
// ✅ 단건 전용 조회 (전 컬럼 + 키로 직접 조회)
|
||||
$version = $this->repository->findVersionByNumber($id, $version);
|
||||
```
|
||||
|
||||
### 대표 1건만 필요하면 관계를 1건으로 좁힌다
|
||||
|
||||
```php
|
||||
// ❌ eager load 의 limit 은 부모별이 아니라 배치 전체에 걸린다
|
||||
// → 첫 주문만 옵션을 받고 나머지는 빈 값
|
||||
->with(['options' => fn ($q) => $q->limit(1)])
|
||||
|
||||
// ✅ 관계 자체를 "가장 오래된 1건" 으로 정의해 부모별 1건 보장
|
||||
->with('firstOption') // hasOne(...)->oldestOfMany()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 목록 조회 컬럼 프루닝과 지연 조인
|
||||
|
||||
@@ -155,6 +155,26 @@
|
||||
권장: 빈 배열/null 전송 방지를 위한 조건부 표현식 사용
|
||||
```
|
||||
|
||||
### 서버 필터와 화면 필터는 같은 출처를 읽는다
|
||||
|
||||
`params` 로 서버에 넘겨 응답을 좁힌 값(채널·상태·분류 등)을 화면이 다시 필터 조건으로 쓴다면, 두 표현식은 **같은 출처 하나**를 읽어야 한다. 표현식을 글자까지 똑같이 적는 것으로는 부족하다 — 출처가 둘이면 갱신 시점이 달라 어긋난다.
|
||||
|
||||
```json
|
||||
// ❌ 두 출처 (URL + 페이지 로컬 상태)
|
||||
"params": { "template_channel": "{{_local.activeChannel ?? query.channel ?? 'mail'}}" }
|
||||
"if": "{{(row.templates ?? []).find(t => t.channel === (_local.activeChannel ?? query.channel ?? 'mail'))}}"
|
||||
|
||||
// ✅ URL 단일 출처
|
||||
"params": { "template_channel": "{{query.channel ?? 'mail'}}" }
|
||||
"if": "{{(row.templates ?? []).find(t => t.channel === (query.channel ?? 'mail'))}}"
|
||||
```
|
||||
|
||||
`_local` 은 화면 안에서만 사는 값이라 URL 이 나르지 못한다. 다른 탭에 갔다 돌아오면 URL 의 필터 키는 사라지는데(탭 전환은 목록 상태를 승계하지 않는다) `_local` 만 이전 값으로 남는다. 그 사이 목록은 기본값으로 다시 불려 오고 화면은 남은 값으로 걸러내므로, **응답에는 있는 데이터를 화면이 "없음" 으로 표시**한다. 예외도 콘솔 경고도 4xx/5xx 도 남지 않아 로그로는 발견되지 않는다.
|
||||
|
||||
같은 이유로 정적 대조 테스트도 이 결함을 놓친다. 두 표현식이 문자열로 동일하기 때문이다. 검사해야 하는 것은 동일성이 아니라 **출처의 개수**다.
|
||||
|
||||
로컬 상태를 함께 두고 싶은 이유가 "클릭 즉시 반응" 이라면, `navigate` 가 URL 을 갱신하면서 이미 같은 렌더 사이클에 반영되므로 별도 복제가 필요 없다.
|
||||
|
||||
### multipart/form-data 지원 (파일 업로드)
|
||||
|
||||
> **버전**: engine-v1.19.0+
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
|
||||
### Added
|
||||
|
||||
- 레이아웃 편집기 버전 이력의 "더 보기" 버튼과 변경내용이 너무 클 때의 안내 문구 일본어 번역 추가.
|
||||
- 버전 목록을 한 번에 몇 개까지 가져올지 정하는 값의 입력 안내 문구 일본어 번역 추가.
|
||||
- 통신이 끊겼을 때 표시되는 네트워크 오류 안내 문구 일본어 번역 추가 — 종전에는 내부 식별 문구가 영문 그대로 노출됐습니다.
|
||||
|
||||
## [1.0.8] - 2026-08-01
|
||||
|
||||
@@ -526,7 +526,7 @@ return [
|
||||
],
|
||||
'notification_channel' => [
|
||||
'invalid' => '通知チャネル識別子が正しくありません。',
|
||||
'unavailable' => "利用できない通知チャネルです: ':value'",
|
||||
'unavailable' => '利用できない通知チャネルです: \':value\'',
|
||||
],
|
||||
'translatable' => [
|
||||
'must_be_array' => '多言語フィールドは配列である必要があります。',
|
||||
@@ -678,7 +678,6 @@ return [
|
||||
'empty_word' => '拡張識別子でアンダースコアを連続させたり両端に置くことはできません。',
|
||||
'word_starts_with_digit' => '拡張識別子の各単語は数字で始めることはできません。',
|
||||
],
|
||||
|
||||
'module_path' => [
|
||||
'file_type_not_allowed' => '許可されていないファイルタイプです。拡張子: :extension(許可: :allowed)',
|
||||
'must_be_string' => 'パスは文字列である必要があります。',
|
||||
@@ -1145,4 +1144,11 @@ return [
|
||||
'log_level' => 'ログレベル',
|
||||
'log_days' => 'ログ保持日数',
|
||||
],
|
||||
'layout_version' => [
|
||||
'limit' => [
|
||||
'integer' => '閲覧数は数字である必要があります。',
|
||||
'min' => '閲覧数は1以上である必要があります。',
|
||||
'max' => '閲覧数は最大:max件です。',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -126,7 +126,8 @@
|
||||
"diff_title": "v{old} → v{new} 変更内容",
|
||||
"diff_back": "一覧",
|
||||
"diff_identical": "2つのバージョンの内容は同じです。",
|
||||
"diff_too_large": "変更内容が大きすぎて表示できません。"
|
||||
"diff_too_large": "変更内容が大きすぎて表示できません。",
|
||||
"load_more": "もっと見る"
|
||||
},
|
||||
"preview_action": {
|
||||
"no_document": "プレビュー用のレイアウトが選択されていません。"
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
|
||||
### Added
|
||||
|
||||
- 관리자 상품 목록에서 행을 펼칠 때 나오는 옵션 불러오기 문구(불러오는 중·실패·재시도·등록된 옵션 없음)의 일본어 번역 추가.
|
||||
- 옵션 일괄 변경 확인 화면에서 펼치지 않은 상품을 개수로 묶어 보여주는 문구와 실패 안내의 일본어 번역 추가.
|
||||
- 상품 옵션 조회 요청 값(상품 ID 목록, 옵션 포함 여부) 검증 안내 문구 일본어 번역 추가.
|
||||
- 무통장입금 자동취소 기한을 비운 채 저장할 때 나오는 안내 문구 일본어 번역 추가.
|
||||
- 유효기간이 설정되지 않은 쿠폰을 발급할 때 나오는 안내 문구 일본어 번역 추가.
|
||||
- 마일리지 설정의 통화별 적립 절사 기준(단위·방식) 항목명과 선택지, 허용값 위반 안내 문구 일본어 번역 추가.
|
||||
|
||||
@@ -58,6 +58,8 @@ return [
|
||||
'code_generation_failed' => '商品コード作成に失敗しました。',
|
||||
'logs_fetch_success' => '商品処理履歴を閲覧しました。',
|
||||
'logs_fetch_failed' => '商品処理履歴の閲覧に失敗しました。',
|
||||
'options_fetch_success' => '商品オプションを閲覧しました。',
|
||||
'options_fetch_failed' => '商品オプション閲覧に失敗しました。',
|
||||
],
|
||||
'presets' => [
|
||||
'created' => '検索プリセットが保存されました。',
|
||||
|
||||
@@ -210,6 +210,9 @@ return [
|
||||
'shipping_policy_id' => [
|
||||
'integer' => '配送ポリシーIDは数字である必要があります。',
|
||||
],
|
||||
'with_options' => [
|
||||
'boolean' => 'オプション含有の有無は true または false である必要があります。',
|
||||
],
|
||||
],
|
||||
'category_required' => 'カテゴリを選択してください。',
|
||||
'category_min' => '最小1個以上のカテゴリを選択してください。',
|
||||
@@ -1772,4 +1775,14 @@ return [
|
||||
'earn_rate_required_when_enabled' => 'マイレージを使用するには、基本積立率は0より大きい必要があります。',
|
||||
'expires_at_invalid' => '有効期限は正しい日付である必要があります。',
|
||||
],
|
||||
'options_list' => [
|
||||
'product_ids' => [
|
||||
'required' => '閲覧する商品を選択してください。',
|
||||
'array' => '商品 ID リストは配列である必要があります。',
|
||||
'min' => '閲覧する商品を1つ以上指定してください。',
|
||||
'max' => '一度に閲覧できる商品は最大 :max 個です。',
|
||||
'integer' => '商品 ID は数字である必要があります。',
|
||||
'item_min' => '商品 ID は1以上である必要があります。',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
+6
-2
@@ -189,7 +189,10 @@
|
||||
"safe_stock": "安全在庫",
|
||||
"active": "使用"
|
||||
},
|
||||
"no_options": "登録されたオプションがありません"
|
||||
"no_options": "登録されたオプションがありません",
|
||||
"loading": "オプションを読み込み中です...",
|
||||
"load_failed": "オプションの読み込みに失敗しました。",
|
||||
"retry": "再試行"
|
||||
},
|
||||
"shipping": {
|
||||
"free": "送料無料",
|
||||
@@ -592,7 +595,8 @@
|
||||
"price_change": "販売価格一括変更",
|
||||
"stock_change": "在庫一括変更",
|
||||
"no_changes": "変更する項目がありません。",
|
||||
"error_section": "変更に失敗しました"
|
||||
"error_section": "変更に失敗しました",
|
||||
"options_aggregate": "{{product}} > オプション {{count}}個すべて"
|
||||
},
|
||||
"bulk_placeholder": {
|
||||
"sales_status": "販売ステータス選択",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
### Added
|
||||
|
||||
- 역할 관리 목록의 「권한 수」 열 제목 일본어 번역 추가.
|
||||
- 확장 업데이트 모달의 「검색 인덱스 재생성」 선택 항목 문구 일본어 번역 추가 — 항목 이름과 설명(재생성 시 인덱스 잠금·재색인 안내)이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
|
||||
## [1.0.4] - 2026-07-17
|
||||
|
||||
@@ -1053,6 +1053,7 @@
|
||||
"name": "ロール名",
|
||||
"description": "説明",
|
||||
"users_count": "ユーザー数",
|
||||
"permissions_count": "権限数",
|
||||
"status": "ステータス",
|
||||
"created_at": "作成日"
|
||||
},
|
||||
|
||||
@@ -1127,6 +1127,15 @@ return [
|
||||
'definition_already_exists' => 'A definition with the same (provider, scope_type, scope_value) already exists.',
|
||||
],
|
||||
|
||||
// Layout version list
|
||||
'layout_version' => [
|
||||
'limit' => [
|
||||
'integer' => 'The limit must be a number.',
|
||||
'min' => 'The limit must be at least 1.',
|
||||
'max' => 'The limit may not be greater than :max.',
|
||||
],
|
||||
],
|
||||
|
||||
// Validation attribute names (validation.attributes)
|
||||
'attributes' => [
|
||||
'ids' => 'user ID list',
|
||||
|
||||
@@ -1120,6 +1120,15 @@ return [
|
||||
'definition_already_exists' => '동일 (provider, scope_type, scope_value) 정의가 이미 존재합니다.',
|
||||
],
|
||||
|
||||
// 레이아웃 버전 목록 조회
|
||||
'layout_version' => [
|
||||
'limit' => [
|
||||
'integer' => '조회 건수는 숫자여야 합니다.',
|
||||
'min' => '조회 건수는 1 이상이어야 합니다.',
|
||||
'max' => '조회 건수는 최대 :max건입니다.',
|
||||
],
|
||||
],
|
||||
|
||||
// 검증 속성명 (validation.attributes)
|
||||
'attributes' => [
|
||||
'ids' => '사용자 ID 목록',
|
||||
|
||||
@@ -126,7 +126,8 @@
|
||||
"diff_title": "Changes v{old} → v{new}",
|
||||
"diff_back": "Back",
|
||||
"diff_identical": "The two versions are identical.",
|
||||
"diff_too_large": "The changes are too large to display."
|
||||
"diff_too_large": "The changes are too large to display.",
|
||||
"load_more": "Load more"
|
||||
},
|
||||
"preview_action": {
|
||||
"no_document": "No layout is selected to preview."
|
||||
|
||||
@@ -126,7 +126,8 @@
|
||||
"diff_title": "v{old} → v{new} 변경 내용",
|
||||
"diff_back": "목록",
|
||||
"diff_identical": "두 버전의 내용이 동일합니다.",
|
||||
"diff_too_large": "변경 내용이 너무 커서 표시할 수 없습니다."
|
||||
"diff_too_large": "변경 내용이 너무 커서 표시할 수 없습니다.",
|
||||
"load_more": "더 보기"
|
||||
},
|
||||
"preview_action": {
|
||||
"no_document": "미리보기할 레이아웃이 선택되지 않았습니다."
|
||||
|
||||
@@ -24,10 +24,12 @@
|
||||
- 신고 관리 목록도 같은 방식으로 개선했습니다. 신고 건수·대상 글 상태처럼 목록에 함께 표시되는 정보를 예전에는 건너뛰는 신고까지 모두 계산했지만, 이제 현재 페이지의 신고에 대해서만 계산합니다. 신고가 많이 쌓인 사이트에서 목록 페이지 이동이 빨라집니다. (#74 @jiwonpapa 님께서 제보해주셨습니다.)
|
||||
- 마이페이지의 「내가 쓴 댓글」·「내가 댓글 단 글」 목록도 같은 방식으로 개선했습니다. 각 글의 최근 댓글을 예전에는 건너뛰는 글까지 모두 찾아봤지만, 이제 현재 페이지의 글에 대해서만 찾습니다. 댓글 활동이 많은 회원의 목록 페이지 이동이 빨라집니다. (#74 @jiwonpapa 님께서 제보해주셨습니다.)
|
||||
- 게시글이 많은 게시판에서 목록 정렬이 더 빨라지도록 색인을 정비했습니다. 신고 관리 목록에도 같은 정비를 적용했습니다. 같은 시각에 등록된 게시글이 많은 구간에서 순서를 정하느라 생기던 추가 작업이 사라집니다. 기존 사이트도 업데이트 시 자동으로 반영되며, 게시글이 아주 많은 경우 이 과정에 수 분이 걸리고 그동안 글쓰기가 잠시 대기할 수 있습니다.
|
||||
- 관리자 게시판 목록이 게시판마다 매니저·스텝 역할과 그 역할에 속한 회원 명단(이름·이메일)을 함께 내려주지 않습니다. 목록 화면이 쓰지 않는 값이라 게시판 수만큼 늘어나던 조회와 전송량이 사라집니다. 역할 지정은 종전대로 게시판 상세·설정 화면에서 하며, 목록에 보이는 항목은 이전과 동일합니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 개선된 사이트맵 생성 기능을 사용하기 위해 이 모듈은 이제 코어 7.0.6 이상이 필요합니다.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 게시판 알림 설정 화면에서 모든 알림의 제목·수신자·활성 여부가 비어 보이고 「이 채널에 대한 템플릿이 없습니다」로만 표시되던 문제를 수정했습니다. 편집 창을 열어도 내용이 채워지지 않았습니다. 채널을 전환하거나 새로고침해도 마찬가지였습니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 게시판 설정의 '새 글 표시 시간'이 저장 직후에는 숫자가 아닌 형태로 다뤄져, 조회 시점에 따라 값의 형태가 달라지던 문제를 수정했습니다. 새 글 표시 여부 판정에 쓰이는 값이므로 항상 숫자로 처리합니다.
|
||||
- 글을 쓰면서 파일을 함께 첨부하면 글은 저장되는데 첨부파일만 사라지던 문제를 수정했습니다. 첨부 개수·용량·형식 검사와 권한 확인은 모두 통과한 뒤 저장 단계에서만 빠져, 등록된 글에 첨부가 하나도 남지 않았습니다. (#81 @jiwonpapa 님께서 제보해주셨습니다.)
|
||||
- 게시판 신고 정책의 자동 숨김 기준 횟수를 0(자동 숨김 사용 안 함)으로 입력할 수 없던 문제를 수정했습니다. 서버는 0을 "사용 안 함"으로 처리하는데 화면에서만 1 이상을 요구해, 0을 입력하면 저장은 되면서도 입력칸이 계속 오류 상태로 남았습니다. (#81 @jiwonpapa 님께서 제보해주셨습니다.)
|
||||
|
||||
@@ -40,53 +40,21 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
_목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
|
||||
|
||||
> **목록은 경량 표현이다.** 관리자 게시판 목록 화면이 쓰는 행 필드는 `id`/`name`/`slug`/`type`/`is_active`/`categories`/`abilities`/`posts_count` 뿐이다. 종전에는 행마다 매니저·스텝 역할에 속한 **모든 사용자(uuid·이름·이메일)** 까지 함께 직렬화하면서 행 수만큼 역할 조회가 추가로 나갔다. 역할/사용자 정보는 게시판 수정 화면(단건 조회 `GET .../admin/boards/{slug}`)이 공급한다.
|
||||
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | `12` | 기본 키 (내부 식별자) |
|
||||
| name | string | `API 문서 샘플 게시판` | 게시판명 (다국어 JSON) |
|
||||
| name | string | `API 문서 샘플 게시판` | 게시판명 (현재 로케일로 해석된 문자열) |
|
||||
| slug | string | `apidoc-sample-board` | 게시판 슬러그 (URL/테이블명) |
|
||||
| is_active | boolean | `true` | active 여부 |
|
||||
| description | string | `` | 게시판 설명 (현재 로케일로 해석된 문자열) |
|
||||
| posts_count | integer | `0` | 게시글 수 (집계) |
|
||||
| type | string | `basic` | 게시판 타입 (basic, gallery, card 등) |
|
||||
| description | string | `` | 게시판 설명 (다국어 JSON) |
|
||||
| per_page | integer | `20` | 페이지당 게시글 수 (PC) |
|
||||
| per_page_mobile | integer | `15` | 페이지당 게시글 수 (Mobile) |
|
||||
| order_by | string | `created_at` | 정렬 기준 (created_at, view_count, title, author) |
|
||||
| order_direction | string | `DESC` | 정렬 방향 (ASC, DESC) |
|
||||
| is_active | boolean | `true` | 활성 여부 |
|
||||
| categories | array | `[]` | 분류 목록 (배열) |
|
||||
| show_view_count | boolean | `true` | 조회수 노출 |
|
||||
| secret_mode | string | `disabled` | 비밀글 설정 (disabled: 사용안함, enabled: 사용함, always: 고정) |
|
||||
| use_comment | boolean | `true` | 댓글 기능 사용 |
|
||||
| use_reply | boolean | `true` | 게시글 답변 기능 사용 (댓글에 대한 답글 아님) |
|
||||
| max_reply_depth | integer | `5` | 답변글 최대 깊이 (1~5) |
|
||||
| use_report | boolean | `true` | 게시글/댓글 신고 기능 사용 |
|
||||
| comment_order | string | `ASC` | 댓글 정렬 순서 (ASC: 오름차순, DESC: 내림차순) |
|
||||
| max_comment_depth | integer | `10` | 대댓글 최대 깊이 (1~10) |
|
||||
| min_title_length | integer | `2` | 최소 제목 글자 수 |
|
||||
| max_title_length | integer | `200` | 최대 제목 글자 수 |
|
||||
| min_content_length | integer | `10` | 최소 게시글 글자 수 |
|
||||
| max_content_length | integer | `10000` | 최대 게시글 글자 수 |
|
||||
| min_comment_length | integer | `2` | 최소 댓글 글자 수 |
|
||||
| max_comment_length | integer | `1000` | 최대 댓글 글자 수 |
|
||||
| blocked_keywords | array | `[]` | 금지어 목록 (배열) |
|
||||
| use_file_upload | boolean | `true` | 파일 업로드 사용 |
|
||||
| max_file_size | integer | `10` | 최대 파일 크기 (MB) |
|
||||
| max_file_count | integer | `5` | max file 개수 (집계) |
|
||||
| allowed_extensions | array | `["jpg","jpeg","png","gif","pdf","zip"]` | 허용 확장자 배열 |
|
||||
| add_to_menu | null | `null` | 관리자 메뉴 등록 여부 (폼 요청에서만 채워지는 토글 초기값 — 조회 응답에서는 항상 null) |
|
||||
| new_display_hours | integer | `24` | 신규 게시글 표시 기간 (시간 단위) |
|
||||
| board_managers | array | `[]` | 게시판 관리자로 지정된 사용자 목록 (manager 역할 사용자의 uuid/name/email — 역할 기반 파생) |
|
||||
| board_steps | array | `[]` | 게시판 승인/처리 담당자(스텝)로 지정된 사용자 목록 (step 역할 사용자의 uuid/name/email — 역할 기반 파생) |
|
||||
| board_manager_ids | array | `[]` | board manager 식별자 배열 (연관 리소스 참조) |
|
||||
| board_step_ids | array | `[]` | board step 식별자 배열 (연관 리소스 참조) |
|
||||
| notify_author | boolean | `true` | 작성자 이메일 알림 (댓글, 대댓글, 답변글, 관리자 처리 시) |
|
||||
| notify_admin_on_post | boolean | `true` | 관리자 이메일 알림 (게시글 등록 시) |
|
||||
| created_at | string | `2026-07-07 09:34:50` | 생성 일시 |
|
||||
| updated_at | string | `2026-07-07 09:34:50` | 최종 수정 일시 |
|
||||
| permissions | null | `null` | 연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) |
|
||||
| category_post_counts | null | `null` | 분류별 게시글 개수 맵 (요청 시 조건부로만 채워지며, 미포함 시 null) |
|
||||
| posts_count | integer | `0` | posts 개수 (집계) |
|
||||
| user_abilities | null | `null` | 현재 사용자의 게시판별 세부 권한 맵 (can_read/can_write/can_read_secret/can_manage 등 — include_user_abilities 요청 시에만 채워지며 미포함 시 null) |
|
||||
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
| is_owner | boolean | `false` | 현재 인증 사용자가 이 게시판의 소유자인지 여부 |
|
||||
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -97,81 +65,32 @@ HTTP/1.1 200
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "게시판 정보를 조회했습니다.",
|
||||
"message": "게시판 목록을 조회했습니다.",
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"id": 12,
|
||||
"name": "API 문서 샘플 게시판",
|
||||
"slug": "apidoc-sample-board",
|
||||
"is_active": true,
|
||||
"type": "basic",
|
||||
"description": "",
|
||||
"per_page": 20,
|
||||
"per_page_mobile": 15,
|
||||
"order_by": "created_at",
|
||||
"order_direction": "DESC",
|
||||
"categories": [],
|
||||
"show_view_count": true,
|
||||
"secret_mode": "{MASKED}",
|
||||
"use_comment": true,
|
||||
"use_reply": true,
|
||||
"max_reply_depth": 5,
|
||||
"use_report": true,
|
||||
"comment_order": "ASC",
|
||||
"max_comment_depth": 10,
|
||||
"min_title_length": 2,
|
||||
"max_title_length": 200,
|
||||
"min_content_length": 10,
|
||||
"max_content_length": 10000,
|
||||
"min_comment_length": 2,
|
||||
"max_comment_length": 1000,
|
||||
"blocked_keywords": [],
|
||||
"use_file_upload": true,
|
||||
"max_file_size": 10,
|
||||
"max_file_count": 5,
|
||||
"allowed_extensions": [
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"pdf",
|
||||
"zip"
|
||||
],
|
||||
"add_to_menu": null,
|
||||
"new_display_hours": 24,
|
||||
"board_managers": [],
|
||||
"board_steps": [],
|
||||
"board_manager_ids": [],
|
||||
"board_step_ids": [],
|
||||
"notify_author": true,
|
||||
"notify_admin_on_post": true,
|
||||
"created_at": "2026-07-08 10:41:34",
|
||||
"updated_at": "2026-07-08 10:41:34",
|
||||
"permissions": null,
|
||||
"category_post_counts": null,
|
||||
"posts_count": 0,
|
||||
"user_abilities": null,
|
||||
"type": "basic",
|
||||
"is_active": true,
|
||||
"categories": [],
|
||||
"is_owner": false,
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"... (총 3건 중 1건 표시)"
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"last_page": 1,
|
||||
"per_page": 25,
|
||||
"total": 1,
|
||||
"from": 1,
|
||||
"to": 1,
|
||||
"has_more_pages": false
|
||||
},
|
||||
"abilities": {
|
||||
"can_create": true,
|
||||
"can_update": true,
|
||||
"can_delete": true
|
||||
"per_page": 20,
|
||||
"total": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2879,7 +2798,7 @@ HTTP/1.1 200
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.write|sirsoft-board.{slug}.manager`)이 없는 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.write| sirsoft-board.{slug}.manager`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
@@ -3333,7 +3252,7 @@ HTTP/1.1 200
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.write|sirsoft-board.{slug}.manager`)이 없는 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.write| sirsoft-board.{slug}.manager`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
@@ -3702,7 +3621,7 @@ HTTP/1.1 200
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.comments.write|sirsoft-board.{slug}.manager`)이 없는 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.comments.write| sirsoft-board.{slug}.manager`)이 없는 경우 |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
<!-- @generated:end -->
|
||||
@@ -3747,7 +3666,7 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
|
||||
|
||||
| 상태코드 | 의미 | 발생 조건 |
|
||||
| --- | --- | --- |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.comments.write|sirsoft-board.{slug}.manager`)이 없는 경우 |
|
||||
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.comments.write| sirsoft-board.{slug}.manager`)이 없는 경우 |
|
||||
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
|
||||
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
|
||||
|
||||
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* @file admin-board-notification-channel-single-source.test.ts
|
||||
* @description 게시판 환경설정 알림 정의 탭 — 채널 표현식 단일 출처 회귀 테스트
|
||||
*
|
||||
* 회귀 배경 (#518 3차 실측 결함 #1):
|
||||
* 알림 정의 탭의 채널은 URL(`query.channel`)과 페이지 로컬 상태(`_local.activeNotificationChannel`)
|
||||
* 두 곳에서 읽히고 있었다. 두 값은 갱신 시점이 다르다 — 다른 설정 탭에 갔다 돌아오면 URL 의
|
||||
* channel 은 사라지지만(탭 전환은 목록 상태를 승계하지 않는다) `_local` 은 이전 채널로 남는다.
|
||||
*
|
||||
* 코어 설정 화면은 그 사이에 목록을 기본 채널로 다시 불러오므로, 남아 있는 이전 채널로 템플릿을
|
||||
* 찾다가 행마다 「이 채널에 대한 템플릿이 없습니다」 를 띄웠다.
|
||||
*
|
||||
* 그래서 조건은 "두 표현식이 같은가" 가 아니라 "출처가 하나인가" 다.
|
||||
*
|
||||
* 2차 회귀 (#518 계획서 검증 브라우저 실측, 2026-08-05):
|
||||
* 목록 프루닝으로 서버가 `template_channel` 을 받은 요청에만 `templates` 를 싣게 됐는데
|
||||
* (`NotificationDefinitionRepository::getPaginated`), 코어 화면만 그 파라미터를 보내도록
|
||||
* 고쳐지고 게시판 탭 데이터소스는 빠졌다. 그 결과 7건 전 행이 「이 채널에 대한 템플릿이
|
||||
* 없습니다」 로 표시되고 수신자·활성 토글·편집 모달 초기값이 통째로 비었다.
|
||||
*
|
||||
* 조용히 깨지는 종류다 — `(def.templates ?? [])` 가드 뒤라 예외도 콘솔 경고도 없고,
|
||||
* 응답은 200 이며 행 수도 정상이라 "템플릿을 아직 안 만들었나 보다" 로 읽힌다.
|
||||
* 그래서 아래 두 단언은 소비와 전송을 **함께** 묶는다: templates 를 읽는 화면이면
|
||||
* 그 채널을 서버에 넘겨야 한다.
|
||||
*
|
||||
* @scenario resource=board_notification_definition,endpoint=admin_settings,observation=consumer_screen
|
||||
* @effects board_notification_channel_reads_single_source, board_notification_list_requests_template_channel, board_notification_reset_button_uses_server_aggregate
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
const SETTINGS_LAYOUT = path.resolve(
|
||||
__dirname,
|
||||
'../../../layouts/admin/admin_board_settings.json',
|
||||
);
|
||||
const NOTIFICATION_TAB = path.resolve(
|
||||
__dirname,
|
||||
'../../../layouts/admin/partials/admin_board_settings/_tab_notification_definitions.json',
|
||||
);
|
||||
|
||||
/**
|
||||
* JSON 트리에서 `{{ }}` 바인딩 표현식을 모두 수집합니다.
|
||||
*
|
||||
* @param node 탐색할 JSON 노드
|
||||
* @param acc 수집 결과 누적 배열
|
||||
* @returns 수집된 바인딩 표현식 목록
|
||||
*/
|
||||
function collectBindings(node: unknown, acc: string[] = []): string[] {
|
||||
if (typeof node === 'string') {
|
||||
const matches = node.match(/\{\{[\s\S]*?\}\}/g);
|
||||
|
||||
if (matches) {
|
||||
acc.push(...matches);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach((child) => collectBindings(child, acc));
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (node && typeof node === 'object') {
|
||||
Object.values(node as Record<string, unknown>).forEach((child) =>
|
||||
collectBindings(child, acc),
|
||||
);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 파일을 읽어 파싱합니다.
|
||||
*
|
||||
* @param filePath 레이아웃 파일 절대 경로
|
||||
* @returns 파싱된 JSON 객체
|
||||
*/
|
||||
function readLayout(filePath: string): Record<string, any> {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
}
|
||||
|
||||
describe('게시판 알림 정의 탭 — 채널 단일 출처 (#518)', () => {
|
||||
it('채널 표현식이 URL 만 읽는다', () => {
|
||||
const bindings = [
|
||||
...collectBindings(readLayout(SETTINGS_LAYOUT)),
|
||||
...collectBindings(readLayout(NOTIFICATION_TAB)),
|
||||
];
|
||||
|
||||
const channelReads = bindings.filter((binding) => /query\.channel/.test(binding));
|
||||
|
||||
expect(
|
||||
channelReads.length,
|
||||
'채널을 읽는 표현식이 있어야 한다 (0건이면 이 단언은 공회전한다)',
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
const dualSourced = channelReads.filter((binding) => /_local\./.test(binding));
|
||||
|
||||
expect(
|
||||
dualSourced,
|
||||
'URL 이 나르지 않는 값(_local)을 채널 출처로 섞으면 데이터와 화면이 서로 다른 채널을 본다',
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('채널 전환은 URL 만 갱신한다 (로컬 상태에 복제하지 않는다)', () => {
|
||||
const raw = fs.readFileSync(NOTIFICATION_TAB, 'utf-8');
|
||||
|
||||
expect(
|
||||
raw.includes('activeNotificationChannel'),
|
||||
'채널을 로컬 상태에 복제하면 URL 과 어긋날 수 있는 두 번째 출처가 다시 생긴다',
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
fs.readFileSync(SETTINGS_LAYOUT, 'utf-8').includes('activeNotificationChannel'),
|
||||
'initLocal 에 남겨두면 소비자가 다시 참조하게 된다',
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('게시판 알림 정의 탭 — 서버 채널 필터 정합 (#518)', () => {
|
||||
/**
|
||||
* 알림 정의 목록 데이터소스를 찾습니다.
|
||||
*
|
||||
* @returns 데이터소스 정의 객체
|
||||
*/
|
||||
function findDefinitionsDataSource(): Record<string, any> {
|
||||
const layout = readLayout(SETTINGS_LAYOUT);
|
||||
const source = (layout.data_sources ?? []).find((ds: Record<string, any>) =>
|
||||
typeof ds.endpoint === 'string' && ds.endpoint.includes('/api/admin/notification-definitions'),
|
||||
);
|
||||
|
||||
expect(source, '알림 정의 목록 데이터소스를 찾지 못했다').toBeTruthy();
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
it('templates 를 그리는 화면이면 목록 요청에 template_channel 을 보낸다', () => {
|
||||
const consumesTemplates = collectBindings(readLayout(NOTIFICATION_TAB)).some((binding) =>
|
||||
/def\.templates/.test(binding),
|
||||
);
|
||||
|
||||
expect(
|
||||
consumesTemplates,
|
||||
'templates 소비가 0건이면 이 단언은 공회전한다 (탭 구조가 바뀌었는지 확인)',
|
||||
).toBe(true);
|
||||
|
||||
// 서버는 `template_channel` 을 받은 요청에만 templates 를 싣는다. 화면이 templates 를
|
||||
// 읽으면서 이 파라미터를 빼면 전 행이 「이 채널에 대한 템플릿이 없습니다」 가 된다.
|
||||
expect(
|
||||
findDefinitionsDataSource().params?.template_channel,
|
||||
'templates 를 소비하는 목록이 채널을 서버에 넘기지 않으면 응답에 templates 키 자체가 없다',
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('서버에 넘기는 채널과 화면이 찾는 채널이 같은 표현식이다', () => {
|
||||
const sent = findDefinitionsDataSource().params.template_channel;
|
||||
|
||||
// 화면이 템플릿을 고를 때 쓰는 채널 표현식을 추출한다.
|
||||
const screenChannel = collectBindings(readLayout(NOTIFICATION_TAB))
|
||||
.find((binding) => /t\.channel === /.test(binding))
|
||||
?.match(/t\.channel === \(([^)]+)\)/)?.[1];
|
||||
|
||||
expect(screenChannel, '화면의 채널 비교 표현식을 찾지 못했다').toBeTruthy();
|
||||
|
||||
// 예: 전송 "{{query.channel ?? 'mail'}}" ↔ 화면 "query.channel ?? 'mail'"
|
||||
expect(
|
||||
sent.replace(/^\{\{|\}\}$/g, '').trim(),
|
||||
'두 채널이 어긋나면 서버가 실어 보낸 채널과 화면이 찾는 채널이 달라 빈 값이 된다',
|
||||
).toBe(screenChannel!.trim());
|
||||
});
|
||||
|
||||
it('되돌리기 버튼은 서버 집계(has_customized_templates)로 판정한다', () => {
|
||||
const raw = fs.readFileSync(NOTIFICATION_TAB, 'utf-8');
|
||||
|
||||
// templates 는 한 채널로 좁혀 실리므로, 배열을 순회해서는 "다른 채널에서 커스터마이즈됨"
|
||||
// 을 알 수 없다. 채널을 가리지 않는 판정은 서버 집계가 유일한 출처다.
|
||||
expect(
|
||||
/def\.templates[\s\S]{0,80}?\.some\(/.test(raw),
|
||||
'되돌리기 조건을 templates 배열 순회로 판정하면 다른 채널의 커스터마이즈를 놓친다',
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
raw.includes('def.has_customized_templates'),
|
||||
'되돌리기 노출 조건이 서버 집계를 읽어야 한다',
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -37,7 +37,6 @@
|
||||
]
|
||||
},
|
||||
"initLocal": {
|
||||
"activeNotificationChannel": null,
|
||||
"editingDefinition": null,
|
||||
"editingTemplate": null,
|
||||
"editingRecipients": [],
|
||||
@@ -128,7 +127,8 @@
|
||||
"page": "{{query.page || 1}}",
|
||||
"per_page": "{{query.per_page || 20}}",
|
||||
"search": "{{query.search}}",
|
||||
"search_type": "{{query.search_type}}"
|
||||
"search_type": "{{query.search_type}}",
|
||||
"template_channel": "{{query.channel ?? 'mail'}}"
|
||||
},
|
||||
"initLocal": {
|
||||
"_merge": "deep",
|
||||
|
||||
+14
-21
@@ -146,7 +146,7 @@
|
||||
},
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "{{(_local.activeNotificationChannel ?? query.channel ?? (availableChannels?.data?.channels ?? [])[0]?.id ?? 'mail') === ch.id ? 'border-b-2 border-indigo-500 dark:border-indigo-400 text-indigo-600 dark:text-indigo-400' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}} px-4 py-2.5 text-sm font-medium whitespace-nowrap flex-center gap-2"
|
||||
"className": "{{(query.channel ?? 'mail') === ch.id ? 'border-b-2 border-indigo-500 dark:border-indigo-400 text-indigo-600 dark:text-indigo-400' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'}} px-4 py-2.5 text-sm font-medium whitespace-nowrap flex-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
@@ -169,13 +169,6 @@
|
||||
"handler": "sequence",
|
||||
"params": {
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"activeNotificationChannel": "{{ch.id}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
@@ -400,16 +393,16 @@
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{(def.templates ?? []).find(t => t.channel === (_local.activeNotificationChannel ?? query.channel ?? 'mail'))}}",
|
||||
"if": "{{(def.templates ?? []).find(t => t.channel === (query.channel ?? 'mail'))}}",
|
||||
"props": {
|
||||
"className": "text-label-subtle truncate"
|
||||
},
|
||||
"text": "{{(def.templates ?? []).find(t => t.channel === (_local.activeNotificationChannel ?? query.channel ?? 'mail'))?.subject?.[$locale] ?? ''}}"
|
||||
"text": "{{(def.templates ?? []).find(t => t.channel === (query.channel ?? 'mail'))?.subject?.[$locale] ?? ''}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{!(def.templates ?? []).find(t => t.channel === (_local.activeNotificationChannel ?? query.channel ?? 'mail'))}}",
|
||||
"if": "{{!(def.templates ?? []).find(t => t.channel === (query.channel ?? 'mail'))}}",
|
||||
"props": {
|
||||
"className": "text-sm text-gray-400 dark:text-gray-500 italic"
|
||||
},
|
||||
@@ -419,7 +412,7 @@
|
||||
"id": "board_recipients_badge_{{defIdx}}",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{((def.templates ?? []).find(t => t.channel === (_local.activeNotificationChannel ?? query.channel ?? 'mail'))?.recipients ?? []).length > 0}}",
|
||||
"if": "{{((def.templates ?? []).find(t => t.channel === (query.channel ?? 'mail'))?.recipients ?? []).length > 0}}",
|
||||
"props": {
|
||||
"className": "flex-center flex-wrap gap-1.5 mt-2"
|
||||
},
|
||||
@@ -436,7 +429,7 @@
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"iteration": {
|
||||
"source": "{{(def.templates ?? []).find(t => t.channel === (_local.activeNotificationChannel ?? query.channel ?? 'mail'))?.recipients ?? []}}",
|
||||
"source": "{{(def.templates ?? []).find(t => t.channel === (query.channel ?? 'mail'))?.recipients ?? []}}",
|
||||
"item_var": "rcpt"
|
||||
},
|
||||
"props": {
|
||||
@@ -505,7 +498,7 @@
|
||||
"name": "notification_definition_row_footer",
|
||||
"props": {
|
||||
"definition": "{{def}}",
|
||||
"activeChannel": "{{_local.activeNotificationChannel ?? query.channel ?? 'mail'}}"
|
||||
"activeChannel": "{{query.channel ?? 'mail'}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -520,9 +513,9 @@
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"if": "{{(def.templates ?? []).find(t => t.channel === (_local.activeNotificationChannel ?? query.channel ?? 'mail'))}}",
|
||||
"if": "{{(def.templates ?? []).find(t => t.channel === (query.channel ?? 'mail'))}}",
|
||||
"props": {
|
||||
"checked": "{{(def.templates ?? []).find(t => t.channel === (_local.activeNotificationChannel ?? query.channel ?? 'mail'))?.is_active ?? false}}",
|
||||
"checked": "{{(def.templates ?? []).find(t => t.channel === (query.channel ?? 'mail'))?.is_active ?? false}}",
|
||||
"disabled": "{{_computed.isNotificationReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
@@ -530,7 +523,7 @@
|
||||
"type": "change",
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "{{'/api/admin/notification-templates/' + (def.templates ?? []).find(t => t.channel === (_local.activeNotificationChannel ?? query.channel ?? 'mail'))?.id + '/toggle-active'}}",
|
||||
"target": "{{'/api/admin/notification-templates/' + (def.templates ?? []).find(t => t.channel === (query.channel ?? 'mail'))?.id + '/toggle-active'}}",
|
||||
"params": {
|
||||
"method": "PATCH"
|
||||
},
|
||||
@@ -581,9 +574,9 @@
|
||||
"params": {
|
||||
"target": "global",
|
||||
"board_notification_template_form_modal": {
|
||||
"template": "{{(def.templates ?? []).find(t => t.channel === (_local.activeNotificationChannel ?? query.channel ?? 'mail'))}}",
|
||||
"template": "{{(def.templates ?? []).find(t => t.channel === (query.channel ?? 'mail'))}}",
|
||||
"definition": "{{def}}",
|
||||
"recipients": "{{JSON.parse(JSON.stringify((def.templates ?? []).find(t => t.channel === (_local.activeNotificationChannel ?? query.channel ?? 'mail'))?.recipients ?? []))}}",
|
||||
"recipients": "{{JSON.parse(JSON.stringify((def.templates ?? []).find(t => t.channel === (query.channel ?? 'mail'))?.recipients ?? []))}}",
|
||||
"editLang": "{{$locale}}",
|
||||
"errors": null,
|
||||
"isSaving": false,
|
||||
@@ -604,7 +597,7 @@
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"if": "{{!def.is_default || (def.templates ?? []).some(t => t.is_default === false)}}",
|
||||
"if": "{{!def.is_default || def.has_customized_templates === true}}",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"disabled": "{{_computed.isNotificationReadOnly}}",
|
||||
@@ -656,7 +649,7 @@
|
||||
"type": "composite",
|
||||
"name": "HtmlContent",
|
||||
"props": {
|
||||
"content": "{{(def.templates ?? []).find(t => t.channel === (_local.activeNotificationChannel ?? query.channel ?? 'mail'))?.body?.[$locale] ?? ''}}",
|
||||
"content": "{{(def.templates ?? []).find(t => t.channel === (query.channel ?? 'mail'))?.body?.[$locale] ?? ''}}",
|
||||
"isHtml": true,
|
||||
"className": "prose dark:prose-invert prose-sm max-w-none bg-gray-50 dark:bg-gray-900/50 rounded-lg p-3 max-h-48 overflow-y-auto dark:text-white"
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Modules\Sirsoft\Board\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Resources\BaseApiCollection;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Modules\Sirsoft\Board\Traits\ChecksBoardPermission;
|
||||
|
||||
@@ -48,8 +48,11 @@ class BoardCollection extends BaseApiCollection
|
||||
public function withPermissions(): array
|
||||
{
|
||||
return [
|
||||
// 목록은 경량 표현만 내려준다. 전체 표현(toArray)은 게시판마다
|
||||
// 매니저/스텝 역할과 그 소속 사용자 전원을 조회해 붙이는데(행당 추가 쿼리),
|
||||
// 관리자 목록 화면은 그 값을 쓰지 않는다 — 역할 편집은 게시판 상세/설정이 공급한다.
|
||||
'data' => $this->collection->map(function ($board) {
|
||||
return new BoardResource($board);
|
||||
return (new BoardResource($board))->toAdminListArray(request());
|
||||
}),
|
||||
'pagination' => $this->resource instanceof LengthAwarePaginator ? [
|
||||
'current_page' => $this->resource->currentPage(),
|
||||
|
||||
@@ -142,6 +142,35 @@ class BoardResource extends BaseApiResource
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 게시판 목록용 경량 배열로 변환합니다.
|
||||
*
|
||||
* 사용자 목록(`toListArray`)보다 표시 항목이 조금 더 많습니다 — 관리자 목록은 유형 뱃지·사용
|
||||
* 여부·분류 개수를 함께 보여줍니다. 그렇다고 전체 표현(`toArray`)을 쓸 수는 없습니다.
|
||||
* 전체 표현은 게시판마다 매니저/스텝 역할과 **그 역할에 속한 사용자 전원(uuid·이름·이메일)** 을
|
||||
* 조회해 붙이는데, 목록 화면은 그 값을 쓰지 않으면서 행 수만큼 추가 쿼리와 개인정보를 싣게 됩니다.
|
||||
* 역할 편집은 게시판 상세/설정 화면이 공급합니다.
|
||||
*
|
||||
* @param Request|null $request HTTP 요청
|
||||
* @return array<string, mixed> 관리자 목록용 배열
|
||||
*/
|
||||
public function toAdminListArray(?Request $request = null): array
|
||||
{
|
||||
$request = $request ?? request();
|
||||
|
||||
return [
|
||||
...$this->toListArray($request),
|
||||
|
||||
'type' => $this->getValue('type'),
|
||||
'is_active' => (bool) $this->getValue('is_active'),
|
||||
'categories' => $this->getValue('categories') ?? [],
|
||||
|
||||
// 관리자 목록 화면은 「등록일」 컬럼을 그린다(admin_board_index.json). `toListArray` 에는
|
||||
// 없는 값이라 여기서 명시적으로 싣는다 — 빠지면 예외도 오류도 없이 그 칸만 공란이 된다.
|
||||
'created_at' => $this->formatDateTimeStringForUser($this->created_at),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 목록 API 응답에 포함할 게시판 정보를 반환합니다 (User용).
|
||||
*
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Board\Tests\Feature\Admin;
|
||||
|
||||
// ModuleTestCase를 수동으로 require (autoload 전에 로드 필요)
|
||||
require_once __DIR__.'/../../ModuleTestCase.php';
|
||||
|
||||
use App\Models\User;
|
||||
use Modules\Sirsoft\Board\Models\Board;
|
||||
use Modules\Sirsoft\Board\Models\BoardType;
|
||||
use Modules\Sirsoft\Board\Tests\ModuleTestCase;
|
||||
|
||||
/**
|
||||
* 관리자 게시판 목록 페이로드 회귀 테스트 (#518).
|
||||
*
|
||||
* 목록 표현(`toAdminListArray`)은 전체 표현(`toArray`)에서 필요한 항목만 골라 담는다.
|
||||
* 골라 담는 목록은 화면이 실제로 그리는 항목의 상위집합이어야 한다 — 빠지면 예외도 오류도 없이
|
||||
* 그 컬럼만 공란으로 렌더된다.
|
||||
*/
|
||||
class AdminBoardListPayloadTest extends ModuleTestCase
|
||||
{
|
||||
protected User $adminUser;
|
||||
|
||||
/**
|
||||
* 테스트 환경 설정
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
BoardType::firstOrCreate(
|
||||
['slug' => 'basic'],
|
||||
['name' => ['ko' => '기본', 'en' => 'Basic'], 'is_active' => true, 'is_default' => true]
|
||||
);
|
||||
|
||||
$this->adminUser = $this->createAdminUser([
|
||||
'sirsoft-board.boards.read',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 목록 행에 등록일이 실린다.
|
||||
*
|
||||
* 관리자 게시판 목록 화면(admin_board_index.json)은 「등록일」 컬럼에 `row.created_at` 을
|
||||
* 그린다. 목록 표현이 이 키를 빼면 화면은 조용히 공란이 된다.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @scenario endpoint=admin_list,role_assignment=unassigned
|
||||
* @effects admin_list_row_carries_created_at
|
||||
*/
|
||||
public function test_admin_board_list_row_carries_created_at(): void
|
||||
{
|
||||
$board = Board::create([
|
||||
'name' => ['ko' => '등록일 확인 게시판'],
|
||||
'slug' => 'created-at-'.uniqid(),
|
||||
'type' => 'basic',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$rows = $this->actingAs($this->adminUser)
|
||||
->getJson('/api/modules/sirsoft-board/admin/boards')
|
||||
->assertStatus(200)
|
||||
->json('data.data');
|
||||
|
||||
$row = collect($rows)->firstWhere('id', $board->id);
|
||||
|
||||
$this->assertNotNull($row, '생성한 게시판이 목록에 있어야 한다');
|
||||
$this->assertArrayHasKey(
|
||||
'created_at',
|
||||
$row,
|
||||
'관리자 목록 화면이 등록일 컬럼을 그리는데 응답에 키가 없으면 공란으로만 렌더된다'
|
||||
);
|
||||
$this->assertNotNull($row['created_at'], '등록일 값이 null 이면 화면은 여전히 공란이다');
|
||||
$this->assertMatchesRegularExpression(
|
||||
'/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/',
|
||||
$row['created_at'],
|
||||
'전체 표현(toArray)과 같은 사용자 타임존 문자열 형식이어야 한다'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 목록 표현은 화면이 쓰지 않는 무거운 항목을 싣지 않는다 (프루닝 유지).
|
||||
*
|
||||
* 등록일 누락을 고치면서 전체 표현으로 되돌리면 게시판마다 매니저/스텝 역할의
|
||||
* 소속 사용자 전원(uuid·이름·이메일)이 다시 실린다 — 행당 추가 쿼리이자 개인정보 노출이다.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @scenario endpoint=admin_list,role_assignment=unassigned
|
||||
* @effects admin_list_omits_role_member_payload
|
||||
*/
|
||||
public function test_admin_board_list_stays_pruned(): void
|
||||
{
|
||||
Board::create([
|
||||
'name' => ['ko' => '프루닝 확인 게시판'],
|
||||
'slug' => 'pruned-'.uniqid(),
|
||||
'type' => 'basic',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$row = $this->actingAs($this->adminUser)
|
||||
->getJson('/api/modules/sirsoft-board/admin/boards')
|
||||
->assertStatus(200)
|
||||
->json('data.data.0');
|
||||
|
||||
foreach (['board_managers', 'board_steps', 'board_manager_ids', 'board_step_ids', 'permissions'] as $key) {
|
||||
$this->assertArrayNotHasKey(
|
||||
$key,
|
||||
$row,
|
||||
"목록 표현에 {$key} 가 다시 실리면 행 수만큼 추가 쿼리가 발생한다"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,6 +216,173 @@ class BoardManagementTest extends ModuleTestCase
|
||||
$this->assertTrue($board->fresh()->is_active);
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 게시판 목록에는 역할 소속 사용자 정보가 실리지 않는다 (#518 / 공개 #76).
|
||||
*
|
||||
* 종전에는 행마다 매니저/스텝 역할을 조회해 그 역할에 속한 **사용자 전원의 uuid·이름·이메일**을
|
||||
* 목록에 실었다. 목록 화면은 그 값을 쓰지 않으면서 행 수만큼 추가 쿼리가 나가고 개인정보가
|
||||
* 노출됐다. 역할 편집은 게시판 상세/설정 화면이 공급한다.
|
||||
*
|
||||
* @scenario endpoint=admin_list,role_assignment=assigned
|
||||
*
|
||||
* @effects admin_list_omits_role_member_payload
|
||||
*/
|
||||
public function test_board_list_omits_role_member_payload(): void
|
||||
{
|
||||
Board::factory()->count(2)->create(['is_active' => true]);
|
||||
|
||||
$response = $this->actingAs($this->adminUser)
|
||||
->getJson('/api/modules/sirsoft-board/admin/boards');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
foreach ($response->json('data.data') as $row) {
|
||||
foreach (['board_managers', 'board_steps', 'board_manager_ids', 'board_step_ids'] as $heavy) {
|
||||
$this->assertArrayNotHasKey($heavy, $row, "게시판 목록에 {$heavy} 가 실리면 안 된다");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 목록 화면이 쓰는 표시 필드는 그대로 남는다 (기능 축소 아님).
|
||||
*
|
||||
* @scenario endpoint=admin_list,role_assignment=unassigned
|
||||
*
|
||||
* @effects admin_list_keeps_display_fields
|
||||
*/
|
||||
public function test_board_list_keeps_admin_display_fields(): void
|
||||
{
|
||||
Board::factory()->create(['is_active' => true]);
|
||||
|
||||
$response = $this->actingAs($this->adminUser)
|
||||
->getJson('/api/modules/sirsoft-board/admin/boards');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$row = $response->json('data.data.0');
|
||||
|
||||
foreach (['id', 'name', 'slug', 'type', 'is_active', 'categories', 'posts_count', 'abilities'] as $field) {
|
||||
$this->assertArrayHasKey($field, $row, "관리자 목록이 쓰는 {$field} 가 사라졌다");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 목록 응답 어디에도 사용자 이메일이 실리지 않는다.
|
||||
*
|
||||
* 키 이름만 검사하면 필드가 다른 이름으로 되살아났을 때 통과한다. 노출되면 안 되는 것은
|
||||
* 특정 키가 아니라 **값**이므로 응답 전문에서 이메일 문자열 자체를 찾는다.
|
||||
*
|
||||
* @effects admin_list_never_exposes_member_email
|
||||
*/
|
||||
public function test_board_list_never_exposes_member_email(): void
|
||||
{
|
||||
$manager = User::factory()->create(['email' => 'board-manager-probe@example.com']);
|
||||
$board = Board::factory()->create(['is_active' => true]);
|
||||
|
||||
$managerRole = Role::firstOrCreate(
|
||||
['identifier' => "sirsoft-board.{$board->slug}.manager"],
|
||||
['name' => json_encode(['ko' => '매니저', 'en' => 'Manager']), 'is_active' => true]
|
||||
);
|
||||
$managerRole->users()->syncWithoutDetaching([$manager->id]);
|
||||
|
||||
$response = $this->actingAs($this->adminUser)
|
||||
->getJson('/api/modules/sirsoft-board/admin/boards');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$this->assertStringNotContainsString(
|
||||
'board-manager-probe@example.com',
|
||||
$response->getContent(),
|
||||
'게시판 목록 응답에 역할 소속 사용자의 이메일이 실렸다'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 목록에서 뺀 역할 배정은 상세 조회가 여전히 공급한다 (기능 축소 아님).
|
||||
*
|
||||
* @scenario endpoint=detail,role_assignment=assigned
|
||||
*
|
||||
* @effects detail_still_provides_role_assignments
|
||||
*/
|
||||
public function test_board_detail_still_provides_role_assignments(): void
|
||||
{
|
||||
$manager = User::factory()->create();
|
||||
$board = Board::factory()->create(['is_active' => true]);
|
||||
|
||||
$managerRole = Role::firstOrCreate(
|
||||
['identifier' => "sirsoft-board.{$board->slug}.manager"],
|
||||
['name' => json_encode(['ko' => '매니저', 'en' => 'Manager']), 'is_active' => true]
|
||||
);
|
||||
$managerRole->users()->syncWithoutDetaching([$manager->id]);
|
||||
|
||||
$response = $this->actingAs($this->adminUser)
|
||||
->getJson("/api/modules/sirsoft-board/admin/boards/{$board->id}");
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$data = $response->json('data');
|
||||
|
||||
$this->assertArrayHasKey('board_manager_ids', $data, '상세에서 역할 배정이 사라지면 설정 화면이 비어 열린다');
|
||||
$this->assertContains($manager->uuid, $data['board_manager_ids']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할이 배정되지 않은 게시판도 상세에서 필드 자체는 유지된다 (빈 배열).
|
||||
*
|
||||
* 필드가 통째로 사라지면 화면이 "아직 안 불러온 상태" 와 "배정이 없는 상태" 를 구분하지
|
||||
* 못한다.
|
||||
*
|
||||
* @scenario endpoint=detail,role_assignment=unassigned
|
||||
*
|
||||
* @effects detail_still_provides_role_assignments
|
||||
*/
|
||||
public function test_board_detail_returns_empty_role_assignments_when_unassigned(): void
|
||||
{
|
||||
$board = Board::factory()->create(['is_active' => true]);
|
||||
|
||||
$response = $this->actingAs($this->adminUser)
|
||||
->getJson("/api/modules/sirsoft-board/admin/boards/{$board->id}");
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$data = $response->json('data');
|
||||
|
||||
$this->assertArrayHasKey('board_manager_ids', $data);
|
||||
$this->assertSame([], $data['board_manager_ids']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 수가 늘어도 역할 조회가 늘지 않는다 (행당 재조회 부재).
|
||||
*
|
||||
* @effects role_query_count_does_not_scale_with_board_count
|
||||
*/
|
||||
public function test_board_list_role_query_count_does_not_grow_with_rows(): void
|
||||
{
|
||||
Board::factory()->count(2)->create(['is_active' => true]);
|
||||
|
||||
// 권한/설정 캐시 워밍 (측정 대상 제외)
|
||||
$this->actingAs($this->adminUser)->getJson('/api/modules/sirsoft-board/admin/boards')->assertOk();
|
||||
|
||||
$queries = [];
|
||||
DB::listen(function ($query) use (&$queries) {
|
||||
$queries[] = $query->sql;
|
||||
});
|
||||
|
||||
$this->actingAs($this->adminUser)->getJson('/api/modules/sirsoft-board/admin/boards')->assertOk();
|
||||
$baseline = count(array_filter($queries, fn (string $sql) => $this->isBoardRoleMemberQuery($sql)));
|
||||
|
||||
Board::factory()->count(8)->create(['is_active' => true]);
|
||||
|
||||
$queries = [];
|
||||
$this->actingAs($this->adminUser)->getJson('/api/modules/sirsoft-board/admin/boards')->assertOk();
|
||||
$grown = count(array_filter($queries, fn (string $sql) => $this->isBoardRoleMemberQuery($sql)));
|
||||
|
||||
$this->assertSame(
|
||||
$baseline,
|
||||
$grown,
|
||||
"게시판 수에 비례해 역할 조회가 늘었다 (기준: {$baseline}, 증가 후: {$grown})"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 목록 조회 시 is_active 필드 포함 테스트
|
||||
*/
|
||||
@@ -779,4 +946,19 @@ class BoardManagementTest extends ModuleTestCase
|
||||
$managerRole->refresh();
|
||||
$this->assertEquals($originalName, $managerRole->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 역할 **소속 사용자 조회** 쿼리인지 판정합니다.
|
||||
*
|
||||
* 권한 체크(`hasPermission`)도 같은 피벗 테이블을 참조하지만 그쪽은 `exists(...)` 서브쿼리이며,
|
||||
* 행마다 실행되는 그 N+1 은 별도 이슈(#519)가 소유한다. 이 테스트는 목록이 게시판마다 역할
|
||||
* 소속 사용자를 다시 끌어오지 않는지만 본다.
|
||||
*
|
||||
* @param string $sql 실행된 SQL
|
||||
* @return bool 역할 소속 사용자 조회 여부
|
||||
*/
|
||||
private function isBoardRoleMemberQuery(string $sql): bool
|
||||
{
|
||||
return str_contains($sql, 'user_roles') && ! str_contains($sql, 'exists(');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\Sirsoft\Board\Http\Resources\BoardResource;
|
||||
use Modules\Sirsoft\Board\Models\Board;
|
||||
use Modules\Sirsoft\Board\Models\BoardType;
|
||||
use Modules\Sirsoft\Board\Services\BoardService;
|
||||
@@ -88,7 +89,8 @@ class BoardCloneTest extends ModuleTestCase
|
||||
* 16-(1)/(2): copyBoard 반환에 board_manager_ids 와 manager 객체 배열이 채워진다.
|
||||
*
|
||||
* @scenario original_permissions=default
|
||||
* @effects clone_includes_manager_ids, clone_includes_manager_objects_with_labels
|
||||
*
|
||||
* @effects clone_includes_manager_ids, clone_includes_manager_objects_with_labels, board_copy_still_resolves_roles
|
||||
*/
|
||||
public function test_copy_board_includes_manager_ids_and_objects(): void
|
||||
{
|
||||
@@ -122,6 +124,7 @@ class BoardCloneTest extends ModuleTestCase
|
||||
* 4-3 형식 일치: copyBoard 의 role 데이터가 BoardResource 산출과 동일 구조여야 한다.
|
||||
*
|
||||
* @scenario original_permissions=default
|
||||
*
|
||||
* @effects clone_role_data_matches_resource_output
|
||||
*/
|
||||
public function test_copy_board_role_data_matches_resource_output(): void
|
||||
@@ -132,7 +135,7 @@ class BoardCloneTest extends ModuleTestCase
|
||||
|
||||
$copyData = $this->boardService->copyBoard($board->id);
|
||||
|
||||
$expected = \Modules\Sirsoft\Board\Http\Resources\BoardResource::getBoardRoleData(
|
||||
$expected = BoardResource::getBoardRoleData(
|
||||
"sirsoft-board.{$board->slug}"
|
||||
);
|
||||
|
||||
@@ -147,6 +150,7 @@ class BoardCloneTest extends ModuleTestCase
|
||||
* manager/step identifier 가 없어야 하고, 비-스코프 역할은 보존되어야 한다.
|
||||
*
|
||||
* @scenario original_permissions=custom
|
||||
*
|
||||
* @effects clone_strips_old_slug_scope_roles, clone_preserves_non_scope_roles
|
||||
*/
|
||||
public function test_copy_board_strips_old_slug_scope_roles_but_keeps_others(): void
|
||||
@@ -188,6 +192,7 @@ class BoardCloneTest extends ModuleTestCase
|
||||
* 422 가 아니라 201 로 생성되고, manager 명단이 원본과 동일하게 승계된다.
|
||||
*
|
||||
* @scenario original_permissions=default
|
||||
*
|
||||
* @effects clone_saved_201, managers_copied
|
||||
*/
|
||||
public function test_clone_form_data_saves_201_and_inherits_managers(): void
|
||||
@@ -225,6 +230,7 @@ class BoardCloneTest extends ModuleTestCase
|
||||
* 역할(sirsoft-board.{src}.manager/step)이 attach 되지 않는다 (교차 게시판 누수 0).
|
||||
*
|
||||
* @scenario original_permissions=custom
|
||||
*
|
||||
* @effects permissions_copied, no_cross_board_role_leak
|
||||
*/
|
||||
public function test_clone_save_does_not_leak_source_scope_roles(): void
|
||||
@@ -260,6 +266,7 @@ class BoardCloneTest extends ModuleTestCase
|
||||
* 동일 권한에 그대로 복사된다. (누수 차단과 별개로, 의도 권한이 실제 승계되는지)
|
||||
*
|
||||
* @scenario original_permissions=custom
|
||||
*
|
||||
* @effects permissions_copied, custom_role_assignment_preserved
|
||||
*/
|
||||
public function test_clone_save_copies_custom_role_assignment(): void
|
||||
@@ -298,6 +305,7 @@ class BoardCloneTest extends ModuleTestCase
|
||||
* 복제 폼 저장이 boolean 검증(422)에 걸리지 않는다.
|
||||
*
|
||||
* @scenario original_permissions=default
|
||||
*
|
||||
* @effects clone_includes_add_to_menu_boolean
|
||||
*/
|
||||
public function test_copy_board_includes_add_to_menu_as_boolean(): void
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 관리자 게시판 목록 역할 페이로드 프루닝 — 목록이 역할·소속 회원 명단을 싣지 않는지 (#518 / 공개 #76).
|
||||
*
|
||||
* 결함: 목록 Collection 이 행마다 `Role::whereIn()->with('users')` 를 돌려 N+1 을 만들고,
|
||||
* 매니저·스텝 역할에 소속된 **모든 사용자의 uuid·이름·이메일**을 응답에 실었다. 화면은
|
||||
* 그 값을 그리지 않는다 — 게시판 이름/식별자/유형/활성여부/분류만 그린다.
|
||||
*
|
||||
* 이 spec 이 브라우저 수준을 담당하는 이유: 서버 단언(BoardManagementTest)은 응답 형태만 본다.
|
||||
* 목록 표현을 경량 경로로 갈아끼우면서 화면이 쓰던 필드를 함께 떨구면 열이 조용히 비는데,
|
||||
* 그건 응답 단언으로는 드러나지 않는다. 여기서는 실제 렌더된 행을 확인한다.
|
||||
*
|
||||
* 시드 의존: 샘플 시드의 게시판 목록(최소 1건).
|
||||
*
|
||||
* @scenario board-admin-list-role-payload-pruning
|
||||
* @axes endpoint=admin_list endpoint=detail
|
||||
* @effects admin_list_omits_role_member_payload admin_list_never_exposes_member_email admin_list_keeps_display_fields detail_still_provides_role_assignments
|
||||
*/
|
||||
import { test, expect, authenticatePage } from '../../fixtures/board-auth';
|
||||
|
||||
const LIST_URL = '/admin/boards';
|
||||
const LIST_API = /\/api\/modules\/sirsoft-board\/admin\/boards(\?|$)/;
|
||||
|
||||
/** 목록에서 빠져야 하는 역할 페이로드 키 */
|
||||
const ROLE_KEYS = [
|
||||
'board_managers',
|
||||
'board_steps',
|
||||
'board_manager_ids',
|
||||
'board_step_ids',
|
||||
];
|
||||
|
||||
test.describe('관리자 게시판 목록 역할 페이로드 (#518)', () => {
|
||||
// @scenario endpoint=admin_list
|
||||
// @effects admin_list_omits_role_member_payload
|
||||
test('목록 응답에 역할 배열과 역할 ID 목록이 없다', async ({ page, boardManageToken }) => {
|
||||
await authenticatePage(page, boardManageToken);
|
||||
|
||||
const listResponse = page.waitForResponse(
|
||||
(res) => LIST_API.test(res.url()) && res.request().method() === 'GET',
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
|
||||
await page.goto(LIST_URL);
|
||||
const payload = await (await listResponse).json();
|
||||
|
||||
const rows = payload?.data?.data ?? [];
|
||||
expect(rows.length, '게시판이 최소 1건은 있어야 판정할 수 있다').toBeGreaterThan(0);
|
||||
|
||||
for (const row of rows) {
|
||||
for (const key of ROLE_KEYS) {
|
||||
expect(row, `목록 행에 ${key} 가 실리면 안 된다`).not.toHaveProperty(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// @scenario endpoint=admin_list
|
||||
// @effects admin_list_never_exposes_member_email
|
||||
test('목록 응답 본문에 회원 이메일이 등장하지 않는다', async ({ page, boardManageToken }) => {
|
||||
await authenticatePage(page, boardManageToken);
|
||||
|
||||
const listResponse = page.waitForResponse(
|
||||
(res) => LIST_API.test(res.url()) && res.request().method() === 'GET',
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
|
||||
await page.goto(LIST_URL);
|
||||
const body = await (await listResponse).text();
|
||||
|
||||
// 키 이름이 아니라 본문 전체를 본다 — 역할 페이로드가 다른 키 이름으로 되살아나도 잡힌다.
|
||||
expect(body, '게시판 목록에 회원 이메일이 실리면 안 된다').not.toMatch(
|
||||
/"email"\s*:\s*"[^"]+@/,
|
||||
);
|
||||
expect(body, '게시판 목록에 회원 uuid 가 실리면 안 된다').not.toMatch(/"uuid"\s*:/);
|
||||
});
|
||||
|
||||
// @scenario endpoint=admin_list
|
||||
// @effects admin_list_keeps_display_fields
|
||||
test('목록이 그리는 열(이름·식별자·유형·활성여부)이 그대로 렌더된다', async ({
|
||||
page,
|
||||
boardManageToken,
|
||||
}) => {
|
||||
await authenticatePage(page, boardManageToken);
|
||||
|
||||
const listResponse = page.waitForResponse(
|
||||
(res) => LIST_API.test(res.url()) && res.request().method() === 'GET',
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
|
||||
await page.goto(LIST_URL);
|
||||
const payload = await (await listResponse).json();
|
||||
await page.waitForLoadState('networkidle', { timeout: 30_000 });
|
||||
|
||||
const first = payload?.data?.data?.[0];
|
||||
expect(first, '첫 행이 있어야 한다').toBeTruthy();
|
||||
|
||||
// 응답 계약 — 화면이 순회하는 6필드
|
||||
for (const key of ['name', 'slug', 'type', 'is_active', 'categories', 'abilities']) {
|
||||
expect(first, `목록이 그리는 필드 ${key} 가 빠졌다`).toHaveProperty(key);
|
||||
}
|
||||
|
||||
// 렌더 결과 — 존재를 먼저 확정한 뒤 값을 본다
|
||||
const rows = page.locator('tbody tr');
|
||||
await expect(rows.first()).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
const slug = String(first.slug);
|
||||
await expect(
|
||||
page.locator('tbody').getByText(slug, { exact: false }).first(),
|
||||
'식별자 열이 비면 경량 전환이 화면을 깎은 것이다',
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
// @scenario endpoint=detail
|
||||
// @effects detail_still_provides_role_assignments
|
||||
test('단건 조회는 역할 정보를 그대로 제공한다 (대체 경로 보존)', async ({
|
||||
page,
|
||||
boardManageToken,
|
||||
}) => {
|
||||
await authenticatePage(page, boardManageToken);
|
||||
|
||||
const listResponse = page.waitForResponse(
|
||||
(res) => LIST_API.test(res.url()) && res.request().method() === 'GET',
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
await page.goto(LIST_URL);
|
||||
const payload = await (await listResponse).json();
|
||||
const slug = payload?.data?.data?.[0]?.slug;
|
||||
expect(slug, '단건 조회 대상 slug 가 필요하다').toBeTruthy();
|
||||
|
||||
// 인증은 Bearer 토큰이다 — 쿠키 세션이 아니므로 헤더를 명시해야 401 이 나지 않는다.
|
||||
const detail = await page.evaluate(
|
||||
async ({ boardSlug, token }) => {
|
||||
const res = await fetch(`/api/modules/sirsoft-board/admin/boards/${boardSlug}`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
return { status: res.status, body: await res.json() };
|
||||
},
|
||||
{ boardSlug: slug, token: boardManageToken },
|
||||
);
|
||||
|
||||
expect(detail.status).toBe(200);
|
||||
// 목록에서 뺀 값은 단건이 공급한다 — 없애는 것이 아니라 옮기는 것이다.
|
||||
expect(detail.body?.data).toHaveProperty('board_manager_ids');
|
||||
expect(detail.body?.data).toHaveProperty('board_step_ids');
|
||||
});
|
||||
});
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 게시판 알림 정의 탭 채널 계약 — 목록 요청이 화면 채널을 나르는지 (#518 / 공개 #76).
|
||||
*
|
||||
* 결함(2026-08-05 브라우저 실측): 목록 프루닝으로 서버가 `template_channel` 을 받은 요청에만
|
||||
* `templates` 를 싣게 됐는데, 코어 설정 화면만 그 파라미터를 보내도록 고쳐지고 게시판 탭
|
||||
* 데이터소스는 빠졌다. 7건 전 행이 「이 채널에 대한 템플릿이 없습니다」로 표시되고
|
||||
* 제목·수신자·활성 토글·편집 모달 초기값이 통째로 비었다.
|
||||
*
|
||||
* 이 spec 이 브라우저 수준을 담당하는 이유: 구조 테스트(Vitest)는 레이아웃 JSON 에
|
||||
* `template_channel` 키가 있는지만 본다. 그 키가 **실제로 요청에 실려 나가는지**와
|
||||
* 그 응답으로 행이 채워지는지는 렌더까지 가야 드러난다 — 결함 당시 응답은 200 이었고
|
||||
* 행 수도 정상이었다.
|
||||
*
|
||||
* 시드 의존: 게시판 모듈 알림 정의(최소 1건). 커스터마이즈 축은 화면에서 만들고 되돌린다.
|
||||
*/
|
||||
import { test, expect, authenticatePage } from '../../fixtures/board-auth';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
const SETTINGS_URL = '/admin/boards/settings?tab=notification_definitions';
|
||||
const DEFINITIONS_API = /\/api\/admin\/notification-definitions(\?|$)/;
|
||||
|
||||
/** 템플릿이 비었을 때 행에 표시되는 문구 */
|
||||
const EMPTY_TEMPLATE_TEXT = '이 채널에 대한 템플릿이 없습니다';
|
||||
|
||||
/**
|
||||
* 알림 정의 목록 응답을 기다리며 해당 채널의 설정 탭으로 이동합니다.
|
||||
*
|
||||
* @param page Playwright 페이지
|
||||
* @param channel 조회할 알림 채널
|
||||
* @returns 요청 URL 과 응답 본문
|
||||
*/
|
||||
async function loadChannel(page: Page, channel: string) {
|
||||
const pending = page.waitForResponse(
|
||||
(res) => DEFINITIONS_API.test(res.url()) && res.request().method() === 'GET',
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
|
||||
await page.goto(`${SETTINGS_URL}&channel=${channel}`);
|
||||
|
||||
const response = await pending;
|
||||
|
||||
return { requestUrl: response.url(), payload: await response.json() };
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 상태(커스터마이즈 없음) 축의 공통 단언을 수행합니다.
|
||||
*
|
||||
* @param page Playwright 페이지
|
||||
* @param channel 검증 대상 채널
|
||||
*/
|
||||
async function assertChannelIsCarried(page: Page, channel: string) {
|
||||
const { requestUrl, payload } = await loadChannel(page, channel);
|
||||
|
||||
expect(
|
||||
requestUrl,
|
||||
'화면이 보는 채널을 서버에 넘기지 않으면 응답에 templates 키 자체가 없다',
|
||||
).toContain(`template_channel=${channel}`);
|
||||
|
||||
const rows = payload?.data?.data ?? [];
|
||||
|
||||
test.skip(rows.length === 0, '게시판 알림 정의 시드가 없어 판정할 수 없다');
|
||||
|
||||
// 서버가 요청한 채널만 실었는지 — 다른 채널이 섞이면 프루닝이 무의미하다.
|
||||
for (const row of rows) {
|
||||
expect(row, 'templates 키가 응답에 있어야 한다').toHaveProperty('templates');
|
||||
|
||||
for (const template of row.templates ?? []) {
|
||||
expect(template.channel, '요청하지 않은 채널이 실렸다').toBe(channel);
|
||||
}
|
||||
}
|
||||
|
||||
// 렌더까지 확인 — 응답에 실렸어도 화면이 다른 채널을 찾으면 빈 문구가 남는다.
|
||||
await expect(
|
||||
page.getByText(EMPTY_TEMPLATE_TEXT).first(),
|
||||
'템플릿이 실렸는데도 빈 문구가 보이면 화면과 요청의 채널이 어긋난 것이다',
|
||||
).toBeHidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* 지정 채널의 첫 정의를 편집해 커스터마이즈 상태로 만든 뒤,
|
||||
* 「기본값 복원」이 채널을 가리지 않고 노출되는지 확인하고 원상 복구합니다.
|
||||
*
|
||||
* @param page Playwright 페이지
|
||||
* @param channel 커스터마이즈를 수행할 채널
|
||||
*/
|
||||
async function assertResetButtonSpansChannels(page: Page, channel: string) {
|
||||
// 편집→저장→재조회→복원까지 도는 다단계 플로우라 기본 30초를 넘는다.
|
||||
test.setTimeout(120_000);
|
||||
|
||||
const { payload } = await loadChannel(page, channel);
|
||||
|
||||
test.skip((payload?.data?.data ?? []).length === 0, '게시판 알림 정의 시드가 없어 판정할 수 없다');
|
||||
|
||||
const resetButton = page.getByRole('button', { name: '기본값 복원' });
|
||||
const initialCount = await resetButton.count();
|
||||
|
||||
await page.getByRole('button', { name: '편집' }).first().click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
const subject = dialog.getByRole('textbox').first();
|
||||
|
||||
await subject.fill(`${await subject.inputValue()} [E2E]`);
|
||||
await dialog.getByRole('button', { name: '저장' }).click();
|
||||
await expect(dialog).toBeHidden({ timeout: 15_000 });
|
||||
|
||||
// 반대 채널로 옮겨도 복원 버튼이 보여야 한다. 화면이 templates 배열을 순회해 판정하면
|
||||
// 반대 채널 templates 에는 커스터마이즈가 없어 버튼이 사라진다.
|
||||
const otherChannel = channel === 'mail' ? 'database' : 'mail';
|
||||
|
||||
try {
|
||||
await loadChannel(page, otherChannel);
|
||||
|
||||
await expect(
|
||||
resetButton.first(),
|
||||
'채널을 가리지 않는 판정은 서버 집계(has_customized_templates)가 유일한 출처다',
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
expect(
|
||||
await resetButton.count(),
|
||||
'커스터마이즈한 정의에만 복원 버튼이 붙어야 한다',
|
||||
).toBeGreaterThan(initialCount);
|
||||
} finally {
|
||||
// 시드 상태로 되돌린다 — 남겨두면 다음 실행의 기준선이 달라진다.
|
||||
await loadChannel(page, otherChannel);
|
||||
|
||||
if ((await resetButton.count()) > 0) {
|
||||
await resetButton.first().click();
|
||||
await page.getByRole('button', { name: '복원', exact: true }).click();
|
||||
await expect(resetButton).toHaveCount(initialCount, { timeout: 15_000 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('게시판 알림 정의 탭 — 채널 계약 (#518)', () => {
|
||||
/**
|
||||
* @scenario channel=mail,template_state=default
|
||||
* @effects board_notification_list_requests_template_channel, board_notification_channel_reads_single_source
|
||||
*/
|
||||
test('mail 채널: 목록 요청이 그 채널을 나르고 행에 템플릿이 채워진다', async ({
|
||||
page,
|
||||
boardManageToken,
|
||||
}) => {
|
||||
await authenticatePage(page, boardManageToken);
|
||||
await assertChannelIsCarried(page, 'mail');
|
||||
});
|
||||
|
||||
/**
|
||||
* @scenario channel=database,template_state=default
|
||||
* @effects board_notification_list_requests_template_channel, board_notification_channel_reads_single_source
|
||||
*/
|
||||
test('database 채널: 목록 요청이 그 채널을 나르고 행에 템플릿이 채워진다', async ({
|
||||
page,
|
||||
boardManageToken,
|
||||
}) => {
|
||||
await authenticatePage(page, boardManageToken);
|
||||
await assertChannelIsCarried(page, 'database');
|
||||
});
|
||||
|
||||
test.describe('커스터마이즈 상태', () => {
|
||||
// 두 축 모두 같은 시드 정의를 편집·복원하므로 병렬로 돌면 서로의 기준선을 무너뜨린다.
|
||||
// 이 블록 안에서만 직렬 — 부모 describe 에 걸면 기본 상태 2건까지 한 건 실패로 중단된다.
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
/**
|
||||
* @scenario channel=mail,template_state=customized
|
||||
* @effects board_notification_reset_button_uses_server_aggregate
|
||||
*/
|
||||
test('mail 채널에서 커스터마이즈하면 database 화면에서도 기본값 복원이 보인다', async ({
|
||||
page,
|
||||
boardManageToken,
|
||||
}) => {
|
||||
await authenticatePage(page, boardManageToken);
|
||||
await assertResetButtonSpansChannels(page, 'mail');
|
||||
});
|
||||
|
||||
/**
|
||||
* @scenario channel=database,template_state=customized
|
||||
* @effects board_notification_reset_button_uses_server_aggregate
|
||||
*/
|
||||
test('database 채널에서 커스터마이즈하면 mail 화면에서도 기본값 복원이 보인다', async ({
|
||||
page,
|
||||
boardManageToken,
|
||||
}) => {
|
||||
await authenticatePage(page, boardManageToken);
|
||||
await assertResetButtonSpansChannels(page, 'database');
|
||||
});
|
||||
});
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
feature: 관리자 게시판 목록의 역할 소속 사용자 프루닝 (#518 / 공개 #76)
|
||||
|
||||
description: |
|
||||
관리자 게시판 목록이 행마다 매니저/스텝 역할을 조회해 **그 역할에 속한 사용자 전원의
|
||||
uuid·이름·이메일**을 응답에 실었다. 목록 화면이 그리는 행 필드는 이름·슬러그·유형·활성
|
||||
여부·분류·권한뿐이라 그 값은 쓰이지 않았고, 게시판 수만큼 추가 쿼리가 나가면서 개인정보인
|
||||
이메일이 함께 노출됐다.
|
||||
|
||||
목록은 화면이 실제로 그리는 것만 싣고, 역할 편집에 필요한 값은 게시판 상세/설정 화면이
|
||||
공급한다. 목록에서 뺀 값이 상세에 남아 있는지를 함께 고정한다 — 남아 있지 않다면 그것은
|
||||
성능 개선이 아니라 기능 삭제다.
|
||||
|
||||
게시판 복사(`copyBoard`)는 Resource 를 거치지 않고 역할 조회 헬퍼를 직접 부른다. 목록에서
|
||||
호출이 사라졌다고 그 헬퍼를 정리하면 복사가 깨지므로 회귀로 함께 고정한다.
|
||||
|
||||
# axis 의미:
|
||||
# 무엇이 실리는지를 가르는 축은 (응답 경로) × (역할 배정 상태) 다.
|
||||
# 역할이 배정되지 않은 게시판은 "빈 배열" 과 "필드 자체 부재" 가 갈리는 지점이라 분리한다.
|
||||
axes:
|
||||
endpoint: [admin_list, detail]
|
||||
role_assignment: [assigned, unassigned]
|
||||
|
||||
effects:
|
||||
- admin_list_omits_role_member_payload
|
||||
- admin_list_never_exposes_member_email
|
||||
- admin_list_keeps_display_fields
|
||||
- admin_list_row_carries_created_at
|
||||
- detail_still_provides_role_assignments
|
||||
- role_query_count_does_not_scale_with_board_count
|
||||
- board_copy_still_resolves_roles
|
||||
|
||||
sub_flows:
|
||||
- id: copy_board_regression
|
||||
description: 목록이 역할 헬퍼를 더 이상 부르지 않아도 게시판 복사는 그대로 동작한다
|
||||
effects:
|
||||
- board_copy_still_resolves_roles
|
||||
|
||||
test_files:
|
||||
- modules/_bundled/sirsoft-board/tests/Feature/Admin/AdminBoardListPayloadTest.php
|
||||
- modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardManagementTest.php
|
||||
- modules/_bundled/sirsoft-board/tests/Feature/BoardCloneTest.php
|
||||
- modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-admin-list-role-payload.spec.ts
|
||||
|
||||
notes: |
|
||||
E2E 를 두는 이유: 변경 자체는 백엔드(Resource/Collection)지만, 목록 표현을 경량 경로로
|
||||
갈아끼우면서 화면이 쓰던 필드를 함께 떨구면 열이 조용히 빈다. 응답 형태만 보는 서버 단언은
|
||||
그것을 잡지 못한다 — 실제 렌더된 행에서 식별자 열이 살아 있는지까지 확인한다.
|
||||
|
||||
이메일 부재는 키 이름이 아니라 **응답 본문 전체**를 정규식으로 본다. 역할 페이로드가 다른
|
||||
키 이름으로 되살아나도 걸리게 하기 위해서다.
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
feature: 게시판 알림 정의 탭의 채널 계약 (#518 / 공개 #76)
|
||||
|
||||
description: |
|
||||
알림 정의 목록은 정의 하나당 (채널 × 로케일) 템플릿 본문을 전부 실었다. 화면은 한 번에 한
|
||||
채널만 그리므로, 서버는 `template_channel` 로 지정된 채널의 템플릿만 싣도록 좁혔다.
|
||||
|
||||
그 좁힘은 **요청과 화면이 같은 채널을 볼 때만** 성립한다. 이 매니페스트가 고정하는 것은
|
||||
두 종류의 어긋남이다.
|
||||
|
||||
1. 화면이 채널을 서버에 넘기지 않는 경우 — 응답에 `templates` 키 자체가 없어 전 행이
|
||||
「이 채널에 대한 템플릿이 없습니다」가 된다. 제목·수신자·활성 토글·편집 모달 초기값이
|
||||
통째로 빈다. `(def.templates ?? [])` 가드 뒤라 예외도 콘솔 경고도 없고 응답은 200,
|
||||
행 수도 정상이라 "템플릿을 아직 안 만들었나 보다" 로 읽힌다.
|
||||
2. 넘기는 채널과 화면이 찾는 채널이 다른 경우 — 서버는 A 채널을 실어 보내고 화면은 B 채널을
|
||||
찾아 같은 증상이 난다. 채널 출처가 URL 하나여야 하는 이유다.
|
||||
|
||||
「기본값 복원」 노출은 채널을 가리지 않는 판정이다. templates 가 한 채널로 좁혀 실리므로
|
||||
배열 순회로는 다른 채널의 커스터마이즈를 알 수 없다 — 서버 집계가 유일한 출처다.
|
||||
|
||||
# axis 의미:
|
||||
# 갈리는 입력은 (화면이 보는 채널) × (그 채널의 템플릿 커스터마이즈 상태) 다.
|
||||
# 채널 전환 경로는 URL 만 바꾸므로 초기 진입과 같은 규칙이고, effects 로 검증한다.
|
||||
axes:
|
||||
channel: [mail, database]
|
||||
template_state: [default, customized]
|
||||
|
||||
effects:
|
||||
- board_notification_channel_reads_single_source
|
||||
- board_notification_list_requests_template_channel
|
||||
- board_notification_reset_button_uses_server_aggregate
|
||||
|
||||
test_files:
|
||||
- modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-notification-channel-single-source.test.ts
|
||||
- modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-notification-definition-channel.spec.ts
|
||||
|
||||
notes: |
|
||||
회귀 이력: 목록 프루닝 도입 시 코어 설정 화면만 `template_channel` 을 보내도록 고쳐지고
|
||||
게시판 탭 데이터소스가 빠져, 7건 전 행이 빈 상태로 표시됐다 (2026-08-05 브라우저 실측).
|
||||
|
||||
그래서 단언은 소비와 전송을 **함께** 묶는다 — templates 를 읽는 표현식이 있는데 데이터소스가
|
||||
채널을 넘기지 않으면 실패한다. 한쪽만 보면 "화면은 templates 를 안 쓴다" 는 잘못된 전제로
|
||||
통과해 버린다.
|
||||
|
||||
활성 토글은 URL 을 `templates[...].id` 로 조립한다. 템플릿이 없으면 그 값이 `undefined` 가
|
||||
되어 존재하지 않는 경로로 요청이 나가므로, 실제 템플릿 ID 로 나가는지까지 축에 포함한다.
|
||||
@@ -25,6 +25,13 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- 관리자 상품 목록이 상품마다 옵션 전체를 함께 내려주지 않습니다. 목록에는 옵션 개수와 재고 합계만 실리고, 옵션 상세는 행을 펼칠 때 필요한 상품의 것만 한 번에 가져옵니다. 옵션을 많이 쓰는 쇼핑몰일수록 목록을 여는 속도와 전송량이 크게 개선됩니다(상품 100건에 상품당 옵션 20건 기준, 목록 응답 크기 약 94% 감소). 화면에서 보이는 내용과 일괄 변경 적용 범위는 이전과 동일하며, 옵션을 목록 응답에 함께 받으려면 주소 뒤에 `?with_options=1` 을 붙이면 됩니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 관리자 주문 목록이 주문마다 주문상품 전체를 함께 내려주지 않습니다. 목록은 대표 상품 1건과 "외 N건" 표기만 사용하므로, 이제 그 1건과 개수만 읽습니다. 한 주문에 상품을 많이 담는 쇼핑몰일수록 목록을 여는 속도와 전송량이 개선됩니다. 화면에 보이는 내용(대표 상품·추가옵션 요약·"외 N건"·부분취소 표시)은 이전과 동일하며, 마이페이지 주문내역은 종전대로 주문상품 전체를 보여줍니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 상품 옵션의 표시 순서를 상품 등록/수정 화면과 동일하게 정렬 순서 기준으로 통일했습니다. 이전에는 관리자 상품 목록의 펼친 옵션만 정해진 순서 없이 표시돼 화면마다 순서가 달라 보일 수 있었습니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 상품 목록에서 대표 이미지를 고를 때 이미 읽어 둔 이미지 정보를 다시 조회하지 않도록 바꿨습니다. 관리자 상품 목록·쇼핑몰 상품 목록·위시리스트·관리자 리뷰 목록이 함께 빨라집니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 관리자 주문 목록이 주문마다 배송 정보 전체를 함께 내려주지 않습니다. 목록은 대표 배송 1건(배송유형·배송방법·택배사·송장번호)만 표시하므로 그 1건만 읽으며, 택배사 이름도 함께 읽어 행마다 다시 조회하지 않습니다. 분할 배송이 많은 쇼핑몰일수록 목록이 가벼워집니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 배송정책 목록이 정책마다 국가별 배송 설정을 통째로 내려주지 않습니다. 목록에는 화면이 실제로 표시하는 값(국가·배송방법·부과정책·배송비·무료배송 기준액·도서산간 사용 여부·사용 여부)만 실리고, 구간별 배송비 표·도서산간 지역 설정·배송비 계산 API 설정은 빠집니다. 정책 하나에 국가 설정이 수백 건까지 달릴 수 있어 목록을 여는 것만으로 그만큼의 상세 설정이 전송됐습니다. 배송정책 관리 목록과 상품 등록/수정 화면의 표시 내용은 종전과 동일하며, 전체 설정이 필요한 외부 연동은 주소 뒤에 `?with_country_settings=1` 을 붙이면 됩니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 마이페이지 주문내역 조회가 기본으로 주문마다 주문상품 전체를 내려주지 않습니다. 기본 응답에는 대표 상품 1건과 전체 개수만 실리며, 주문상품을 전부 나열하는 마이페이지 화면은 전량을 함께 요청하므로 보이는 내용은 종전과 동일합니다. 주문내역 조회를 직접 사용하는 외부 연동에서 상품 전체가 필요하면 주소 뒤에 `?with_items=1` 을 붙이면 됩니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 실제로 사용되지 않던 설정 항목(상품·주문 목록의 페이지당 개수, 결제 자동취소 기한 중복 항목)을 정리했습니다. 미입금 자동취소 기한은 관리자 환경설정의 값 하나만 사용합니다. (#81 @jiwonpapa 님께서 제보해주셨습니다.)
|
||||
- 사이트맵을 만들 때 상품과 카테고리를 한 번에 모두 메모리에 올리지 않고 나눠서 읽도록 바꿨습니다. 상품이 많은 쇼핑몰에서 사이트맵 생성 중 메모리가 부족해 실패하던 문제가 줄어듭니다.
|
||||
- 상품 리뷰·마일리지 내역·쿠폰 발급 내역 목록도 같은 방식으로 뒤쪽 페이지 조회를 개선했습니다. 상품 리뷰는 작성일 정렬용 인덱스를 추가하고 기간 검색 조건을 인덱스가 활용되는 형태로 바꿔, 리뷰가 많은 쇼핑몰에서 목록과 기간 검색이 함께 빨라집니다. (#74 @jiwonpapa 님께서 제보해주셨습니다.)
|
||||
@@ -42,6 +49,8 @@
|
||||
- 할인코드 목록에서 선택 개수·전체 건수·페이지당 표시 개수 문구에 숫자가 채워지지 않고 자리표시자가 그대로 보이던 문제를 수정했습니다.
|
||||
- 마이페이지와 비회원 주문 조회의 주문 상세에서 상품별 배송정책명과 개별 배송비가 아무 안내 없이 표시되지 않던 문제를 수정했습니다. 주문에 보관된 배송정책 기록의 저장 형태가 화면이 기대하는 형태와 달라 해당 줄이 통째로 빠져 있었습니다. 이미 저장된 주문도 업그레이드 시 함께 정리됩니다.
|
||||
- 비회원 주문 조회의 주문 상세에서 상품별 배송정책명과 개별 배송비가 표시되지 않던 문제를 추가로 수정했습니다. 회원 주문 상세와 같은 화면 구성을 쓰는데 비회원 응답에만 배송정책 정보가 빠져 있어, 오류 없이 그 줄만 비어 있었습니다. 표시에 필요한 정책명과 배송비만 내려주며 내부 계산 정보는 종전대로 제공하지 않습니다.
|
||||
- 쇼핑몰 알림 설정 화면에서 모든 알림의 제목·수신자·활성 여부가 비어 보이고 「이 채널에 대한 템플릿이 없습니다」로만 표시되던 문제를 수정했습니다. 편집 창을 열어도 내용이 채워지지 않았습니다. 채널을 전환하거나 새로고침해도 마찬가지였습니다. (#76 @jordy-bitree 님께서 제보해주셨습니다.)
|
||||
- 관리자 상품 목록의 새로고침·상품등록·엑셀 내려받기 아이콘이 의도한 크기보다 작게 표시되던 문제와, 일괄적용 버튼이 다크 모드에서 밝은 배경 그대로 보이던 문제를 수정했습니다.
|
||||
- 미입금 자동취소 기한을 비운 채 저장하면 "저장되었습니다"라고 안내되면서도 값은 사라지던 문제를 수정했습니다. 숫자 항목에 숫자가 아닌 값을 입력하면 입력칸이 비워지는데, 그대로 저장하면 설정이 없는 상태로 남아 화면에 보이는 값과 실제 동작이 어긋났습니다. 이제 비워 둔 채로는 저장되지 않고 값을 입력하라고 안내합니다.
|
||||
- 미입금 자동취소 기한 입력 안내가 실제 입력 하한과 다른 "0일 이상"으로 표시되던 문구를 "1일 이상"으로 바로잡았습니다.
|
||||
- 쿠폰 수정 화면에서 유효기간을 "발급일로부터"로 두고 일수를 비운 채 저장할 수 있던 문제를 수정했습니다. 그렇게 저장된 쿠폰은 발급되는 즉시 만료 상태가 되어, 받은 회원이 사용할 수 없으면서도 아무 오류가 나지 않았습니다. 이제 저장 단계에서 일수를 입력하도록 안내하고, 이미 그렇게 저장돼 있던 쿠폰은 발급 시 유효기간이 설정되지 않았다고 알려 줍니다.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"identifier": "sirsoft-ecommerce",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.5",
|
||||
"components": {
|
||||
"basic": [],
|
||||
"composite": [],
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
||||
> 아래 표는 자동 생성됩니다. 각 문서를 열면 엔드포인트별 파라미터·응답·예시를 볼 수 있습니다.
|
||||
|
||||
<!-- @generated:start:api-readme-index -->
|
||||
- **문서 수**: 33 · **엔드포인트 수**: 231
|
||||
- **문서 수**: 33 · **엔드포인트 수**: 232
|
||||
|
||||
| 문서 | 도메인 | 엔드포인트 |
|
||||
| --- | --- | --- |
|
||||
@@ -31,7 +31,7 @@
|
||||
| [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 |
|
||||
| [products.md](products.md) | `products` | 31 |
|
||||
| [promotion-coupons.md](promotion-coupons.md) | `promotion-coupons` | 9 |
|
||||
| [review-image.md](review-image.md) | `review-image` | 1 |
|
||||
| [reviews.md](reviews.md) | `reviews` | 12 |
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
```text
|
||||
1. 이 문서는 실제 API 호출로 실측한 Addresses 엔드포인트 레퍼런스입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 실측 응답 필드 표
|
||||
3. 응답 필드의 예시값은 실제 호출 응답에서 관측된 값입니다
|
||||
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
|
||||
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
|
||||
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
|
||||
5. 설명(TODO) 칸은 사람이 채웁니다
|
||||
```
|
||||
@@ -42,7 +42,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| addresses | object | `{"data":[{"id":263,"user_id":"a1e0a91a-fba6-491c-a53e-728…` | 회원 본인 소유 배송지 컬렉션 (`data[]` 배송지 항목 배열 + `abilities.can_create` — UserAddressCollection 파생) |
|
||||
| addresses | object | `{"data":[],"abilities":{"can_create":true}}` | 회원 본인 소유 배송지 컬렉션 (`data[]` 배송지 항목 배열 + `abilities.can_create` — UserAddressCollection 파생) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -56,39 +56,7 @@ HTTP/1.1 200
|
||||
"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
|
||||
}
|
||||
}
|
||||
],
|
||||
"data": [],
|
||||
"abilities": {
|
||||
"can_create": true
|
||||
}
|
||||
@@ -174,7 +142,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| address | object | `{"id":9,"user_id":"a234c2b1-cde8-437f-b28b-23323be2b98d",…` | 기본주소 (국내) |
|
||||
| address | object | `{"id":99,"user_id":"a26219fc-94a0-4f63-9404-04c2a6ac99e4"…` | 기본주소 (국내) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -188,8 +156,8 @@ HTTP/1.1 201
|
||||
"message": "배송지가 추가되었습니다.",
|
||||
"data": {
|
||||
"address": {
|
||||
"id": 9,
|
||||
"user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d",
|
||||
"id": 99,
|
||||
"user_id": "a26219fc-94a0-4f63-9404-04c2a6ac99e4",
|
||||
"name": "실측 예시값",
|
||||
"recipient_name": "실측 예시값",
|
||||
"recipient_phone": "실측 예시값",
|
||||
@@ -210,8 +178,8 @@ HTTP/1.1 201
|
||||
"is_domestic": true,
|
||||
"is_international": false,
|
||||
"full_address": "실측 예시값 실측 예시값",
|
||||
"created_at": "2026-07-08 15:00:36",
|
||||
"updated_at": "2026-07-08 15:00:36",
|
||||
"created_at": "2026-08-05 09:46:56",
|
||||
"updated_at": "2026-08-05 09:46:56",
|
||||
"abilities": {
|
||||
"can_update": true,
|
||||
"can_delete": false,
|
||||
@@ -249,7 +217,7 @@ HTTP/1.1 201
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
DELETE /api/modules/sirsoft-ecommerce/user/addresses/1 HTTP/1.1
|
||||
DELETE /api/modules/sirsoft-ecommerce/user/addresses/{id} HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -257,23 +225,11 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
|
||||
|
||||
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "배송지가 삭제되었습니다.",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -302,7 +258,7 @@ HTTP/1.1 200
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
GET /api/modules/sirsoft-ecommerce/user/addresses/1 HTTP/1.1
|
||||
GET /api/modules/sirsoft-ecommerce/user/addresses/{id} HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -310,57 +266,11 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| address | object | `{"id":1,"user_id":"a234c2b1-cde8-437f-b28b-23323be2b98d",…` | 기본주소 (국내) |
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -402,14 +312,12 @@ HTTP/1.1 200
|
||||
| intl_postal_code | body | string | 아니오 | max 20 | 우편번호 (국제 주소) |
|
||||
| is_default | body | boolean | 아니오 | — | 기본값 지정 여부 |
|
||||
|
||||
개별 필드는 모두 선택이지만 주소 필수 조합은 "기존 저장값 + 이번 요청" 결과 상태 기준으로 검증됩니다. 국내(`country_code=KR` 또는 미전송 시 기존 국가가 KR)는 `zipcode` + `address`, 해외는 `address_line_1` + `intl_city` + `intl_postal_code` 가 결과적으로 모두 채워져 있어야 하며, 그렇지 않으면 422 로 거절됩니다. 요청에 없는 필드는 저장된 값으로 판정하므로 `is_default` 만 바꾸는 요청이나 도로명 주소만 교체하는 요청은 그대로 통과합니다. 해외 주소의 저장 컬럼은 `city`/`state`/`postal_code` 이므로 요청에 `intl_city`/`intl_postal_code` 가 없으면 그 저장값이 판정 기준이 됩니다.
|
||||
|
||||
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.user_address.update_validation_rules`).
|
||||
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PUT /api/modules/sirsoft-ecommerce/user/addresses/1 HTTP/1.1
|
||||
PUT /api/modules/sirsoft-ecommerce/user/addresses/{id} HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -437,57 +345,11 @@ Content-Type: application/json
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| address | object | `{"id":1,"user_id":"a234c2b1-cde8-437f-b28b-23323be2b98d",…` | 기본주소 (국내) |
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -517,7 +379,7 @@ HTTP/1.1 200
|
||||
**요청 예시**
|
||||
|
||||
```http
|
||||
PATCH /api/modules/sirsoft-ecommerce/user/addresses/1/default HTTP/1.1
|
||||
PATCH /api/modules/sirsoft-ecommerce/user/addresses/{id}/default HTTP/1.1
|
||||
Host: api.example.com
|
||||
Accept: application/json
|
||||
Authorization: Bearer {YOUR_TOKEN}
|
||||
@@ -525,23 +387,11 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
|
||||
|
||||
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
|
||||
**응답 예시**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "기본 배송지로 설정되었습니다.",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
|
||||
**에러 응답**
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user