feat(core,extensions): 확장 문서 호칭 통일과 레이아웃 편집기 문서 축 신설

확장 문서의 제품 호칭을 「그누보드7」로 통일했다. 가 지적한 두 문구는
개별 오타가 아니라 생성기가 찍는 정형 문구여서, 산출물이 아니라 방출 지점
세 곳을 먼저 고쳤다 — 그러지 않으면 21번째 확장부터 다시 샌다. 정리 범위는
확장 문서와 그 생성기까지이며, 코어 docs 와 언어팩 표시명은 의도적으로 남긴다.

레이아웃 편집기 대응 문서가 없던 문제는 확장마다 docs/editor-spec.md 를
두어 닫았다. 실측은 EditorSpecCollector 가 유지하며, 합본에 런타임과 같은
EditorSpecAssembler 를 써서 문서가 말하는 스펙과 편집기가 읽는 스펙이
갈라질 경로를 두지 않았다. 스펙을 두지 않은 확장에도 문서를 둔다 — 미보유가
정상일 수 있고, 그 정상 여부를 적을 자리가 없으면 다음 사람이 부재를 누락으로
오해하거나 필요한 시점을 놓친다.

초안이 낸 수치 다섯 건이 틀렸고 전부 오류 없이 "사실" 로 실릴 값이었다.
블록 최상위 키를 세어 팔레트가 79 대신 3 이 되던 것, `_` 접두 일괄 배제가
실제 항목을 삼키던 것 등을 정정했다.

게이트를 인위적으로 깨뜨려 점검한 결과 사각 하나가 드러났다 — 채워 넣으라는
표시만 지우고 서술을 쓰지 않으면 검사를 통과해, 빈 문서가 완비로 집계됐다.
미채움을 두 축으로 만들어 닫았고 되돌림으로 검산했다. 편집기 스펙 편집 시
규정이 주입되지 않던 것과 스캐폴딩 안내가 새 문서를 빠뜨리던 것도 함께 고쳤다.
This commit is contained in:
HeuJung
2026-09-01 12:02:55 +09:00
parent eb7ff4ec6a
commit 3a0aba85bc
114 changed files with 3433 additions and 129 deletions
+7
View File
@@ -502,11 +502,18 @@ Icon 은 `<i>` 글리프라 박스 크기가 곧 `font-size` 다. `w-N h-N` 은
| 신규 확장을 문서 없이 스캐폴딩 | `php artisan ext:docgen --scope={type}:{id} --init` 으로 골격을 함께 만든다 — 없으면 21번째 확장부터 다시 문서 없이 태어난다 |
| 확장 문서를 활성 디렉토리에서 작성 | `_bundled` 에서만 작성하고 update 커맨드로 반영 (문서만이면 빌드 불필요) |
| 확장이 훅을 추가할 때 코어 문서를 고침 | 훅 집계는 그 확장의 `docs/extension-points.md` 소유 — 코어에는 총계와 링크만 |
| 레이아웃에 `data_source` 를 추가하고 `editor-spec.json` 의 `sampleData` 를 그대로 둠 | 같은 ID 로 프리뷰 샘플 추가 — 없으면 **편집기 캔버스에서만** 그 영역이 빈 화면이 되고 실제 화면은 정상이라 오류도 경고도 남지 않는다 |
| 컴포넌트를 추가하고 팔레트에만 등록 | 템플릿 스펙은 `componentPalette.entries` · `componentPalette.groups` · `nesting` · `componentCapabilities` **넷 다** — 하나만 빠지면 편집기에서 절반만 동작하고, 어느 단계가 빠졌는지는 증상으로만 구분된다 |
| 모듈·플러그인 스펙에 `componentPalette` 선언 | 컴포넌트는 템플릿 소유 — 모듈·플러그인 스펙은 도메인 데이터(`sampleData`·`states`)만 담는다. 같은 자리를 두고 다투면 어느 쪽이 이기는지가 병합 순서에 좌우된다 |
| 공용 ID(`settings`·`roles`·`me`)를 확장마다 각자 선언 | 템플릿 스펙 한 곳 — 사본이 갈라져도 오류가 나지 않는다 |
| 편집기 스펙을 고치고 update 커맨드 생략 | 서빙은 **활성 디렉토리만** 읽는다(`_bundled` 폴백 없음) — 파일은 고쳤는데 편집기에 직전 내용이 그대로 보인다 |
이 결함군은 오류를 남기지 않는다. 문서가 코드와 어긋난 채로 계속 읽히는 것이 유일한 증상이며, 훅 이름이 어긋나면 그 확장을 잡으려던 쪽이 **잡히지 않는 훅을 구독**하게 된다(예외도 경고도 없이 리스너가 호출되지 않을 뿐이다).
mermaid 문법 오류는 GitHub 렌더 시점에만 드러난다. 구조 검사가 잡을 수 있는 것은 선언된 다이어그램 종류·빈 본문·괄호 균형까지이므로, 새 형식은 실제 렌더를 눈으로 확인한다.
`docs/editor-spec.md` 는 세 유형 공통이다 — 편집기 스펙을 두지 않는 확장에도 문서를 둔다. 미보유가 정상일 수 있고("이 확장은 공용 ID 만 쓴다") 그 정상 여부를 적을 자리가 없으면 다음 사람이 부재를 누락으로 오해하거나 필요한 시점을 놓친다. 그 문서의 "샘플 데이터와 페이지 상태" 절은 그 확장 레이아웃의 `data_source` 중 프리뷰 샘플이 붙지 않는 것을 실측해 나열한다 — 이 결함은 편집기 캔버스에서만 빈 화면으로 나타나므로 그 목록이 유일한 통로다.
> 상세: [extension-documentation.md](docs/extension/extension-documentation.md)
> 정적 검사가 확장 표면 변경 시 문서 미동반과 미채움 마커 잔존을 검출한다. 생성기의 비파괴 계약(블록 밖 손실 0 · 재실행 멱등 · 미존재 키 미주입)과 필수 문서·섹션·블록 목록은 테스트가 잠근다.
+4
View File
@@ -24,11 +24,15 @@
- 사이트가 쓰는 외부 라이브러리의 알려진 취약점을 한 번에 점검하는 명령이 추가되었습니다. `php artisan security:audit-dependencies` 로 코어와 설치된 모든 확장을 함께 확인할 수 있고, 개발 대시보드에서도 실행할 수 있습니다. 점검 도구가 원리상 볼 수 없는 동봉 라이브러리는 버전 목록으로 함께 표시해 운영자가 직접 확인할 수 있게 했습니다. (#126 @jiwonpapa 님께서 제보해주셨습니다.)
- 확장(모듈·플러그인·템플릿)이 개발자 문서를 갖추기 위한 체계가 마련되었습니다. 확장마다 `AGENTS.md`(확장을 고치는 사람용 — 설계 의도·확장점·수정 시 동반 의무·금지 패턴)와 `README.md`(도입 검토·운영자용 — 기능·설치·사용 방법·트러블슈팅), `docs/` 상세 문서를 두는 형식을 정의했으며, **동봉된 확장 20개 전부에 문서가 채워졌습니다.** 새 확장을 만들면 스캐폴딩 단계에서 이 문서 골격이 함께 생성됩니다.
- 확장 문서에서 코드로 확인되는 부분(발행·구독 훅, 라우트, 권한, 메뉴, 설정 항목, 모델과 테이블, 레이아웃, 액션 핸들러, 테스트 실행 경로, 다른 확장과의 의존 관계)을 `php artisan ext:docgen` 이 자동으로 채우고 유지합니다. 사람이 쓴 서술은 손대지 않고 자동 생성 표만 교체하며, `php artisan ext:docgen --check` 로 문서가 코드와 어긋났는지 확인할 수 있습니다. 개발 대시보드에서도 실행할 수 있습니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목이 추가되었습니다. 그 확장이 레이아웃 편집기에 무엇을 선언했는지(추가 가능한 화면 요소, 스타일 조절 항목, 미리보기용 샘플 데이터, 화면 상태)와 화면 요소·데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담으며, 편집기 스펙을 두지 않은 확장에는 그것이 정상인지 아닌지를 적습니다.
- 확장 문서 검사가 「설명 자리를 비워 둔 상태」도 미작성으로 셉니다. 종전에는 채워 넣으라는 표시만 지우고 내용을 쓰지 않으면 검사를 통과해, 빈 문서가 완비된 것으로 집계되었습니다.
- 확장의 화면에 데이터를 붙였는데 레이아웃 편집기 미리보기에서 그 자리가 비는 경우를 문서가 실측해 알려 줍니다. 이 어긋남은 실제 화면이 정상 동작해 아무 오류도 남지 않으므로 종전에는 편집기를 열어 보기 전까지 드러나지 않았습니다.
### Changed
- 템플릿 컴포넌트 정의·다국어·라우트 응답에 조건부 캐시(ETag)가 적용되어, 변경이 없으면 본문 전송 없이 캐시를 재사용합니다. (#122 @glitter-gim 님께서 건의해주셨습니다.)
- 레이아웃 편집기를 여는 중 네트워크가 잠시 끊겨도 자동으로 다시 시도합니다. 끝내 실패하면 내부 파일 경로 대신 다음에 무엇을 하면 되는지를 안내합니다.
- 확장 문서에서 제품을 가리키는 이름이 「그누보드7」로 통일되었습니다. 종전에는 같은 문서 안에서도 약칭과 정식 명칭이 섞여, 확장만 내려받은 사람에게 별개 제품처럼 보였습니다.
- 번들 템플릿의 컴포넌트·핸들러·레이아웃 상세 문서와 확장이 사용하는 활동 로그 항목 목록이 각 확장의 문서로 옮겨졌습니다. 확장이 기능을 늘릴 때 코어 문서를 함께 고쳐야 하던 의존이 사라졌으며, 코어 문서에는 총계와 각 확장 문서로의 링크만 남습니다.
### Fixed
@@ -2,14 +2,9 @@
namespace App\Console\Commands\Extension;
use App\Support\ExtensionDoc\DataModelCollector;
use App\Support\ExtensionDoc\DeclarativeSurfaceCollector;
use App\Support\ExtensionDoc\DependencyGraphCollector;
use App\Support\ExtensionDoc\ExtensionDocContext;
use App\Support\ExtensionDoc\ExtensionDocScaffolder;
use App\Support\ExtensionDoc\ExtensionInventory;
use App\Support\ExtensionDoc\FrontendInventory;
use App\Support\ExtensionDoc\HookInventory;
use App\Support\ExtensionDoc\TestPathCollector;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use InvalidArgumentException;
@@ -49,23 +44,13 @@ class ExtDocgenCommand extends Command
* 커맨드를 실행합니다.
*
* @param ExtensionInventory $inventory 번들 확장 인벤토리
* @param DeclarativeSurfaceCollector $surface 선언형 표면 수집기
* @param HookInventory $hooks 훅 인벤토리
* @param DataModelCollector $data 데이터 모델 수집기
* @param FrontendInventory $frontend 프론트 인벤토리
* @param TestPathCollector $tests 테스트 경로 수집기
* @param DependencyGraphCollector $deps 의존 관계 수집기
* @param ExtensionDocContext $context 수집 컨텍스트 조립기
* @param ExtensionDocScaffolder $scaffolder 문서 스캐폴더
* @return int 종료 코드
*/
public function handle(
ExtensionInventory $inventory,
DeclarativeSurfaceCollector $surface,
HookInventory $hooks,
DataModelCollector $data,
FrontendInventory $frontend,
TestPathCollector $tests,
DependencyGraphCollector $deps,
ExtensionDocContext $context,
ExtensionDocScaffolder $scaffolder
): int {
$scope = (string) $this->option('scope');
@@ -134,19 +119,7 @@ class ExtDocgenCommand extends Command
$results = [];
foreach ($records as $record) {
// 선언형 표면을 먼저 모은다 — 발행 훅의 1차 출처가 그 안의 `getHooks()` 선언이다.
$collectedSurface = $surface->collect($record);
$declaredHooks = $collectedSurface['values']['getHooks'] ?? [];
$ctx = [
'record' => $record,
'surface' => $collectedSurface,
'hooks' => $hooks->collect($record, is_array($declaredHooks) ? $declaredHooks : []),
'data' => $data->collect($record),
'frontend' => $frontend->collect($record),
'tests' => $tests->collect($record),
'deps' => $deps->collect($record),
];
$ctx = $context->build($record);
$results[] = $this->processExtension($ctx, $scaffolder);
}
@@ -229,6 +202,18 @@ class ExtDocgenCommand extends Command
}
}
// 마커를 지우기만 하고 서술을 안 쓴 자리도 미채움이다. 이 축이 없으면
// "TODO 를 삭제한 문서" 가 "채운 문서" 와 같은 모양으로 통과한다.
$emptyIntents = ExtensionDocScaffolder::emptyIntentBlocks($content);
if ($emptyIntents > 0) {
$result['unfilled'][] = [
'doc' => $doc,
'marker' => ExtensionDocScaffolder::EMPTY_INTENT_LABEL,
'count' => $emptyIntents,
];
}
$docBodies = [];
foreach (ExtensionDocScaffolder::blocksFor($doc) as $key) {
if (array_key_exists($key, $bodies)) {
@@ -0,0 +1,566 @@
<?php
namespace App\Support\ExtensionDoc;
use App\Extension\Helpers\EditorSpecAssembler;
/**
* 확장 편집기 스펙 수집기
*
* 확장이 `editor-spec.json`(+ 분할 `editor-spec/*.json`)으로 레이아웃 편집기에 선언한
* 표면을 실측합니다. 팔레트 항목·스타일 컨트롤·중첩 규칙·샘플 데이터·레시피가 각각 몇
* 개이고 어떤 ID 를 갖는지가 산출물입니다.
*
* 이 축이 문서에 없으면 확장에 새 화면 요소를 추가해도 편집기 팔레트에 나타나지 않는
* 상태가 오류도 경고도 없이 남습니다 — 편집기는 선언되지 않은 컴포넌트를 "없는 것" 으로
* 다룰 뿐 실패를 보고하지 않기 때문입니다. 스펙을 **갖지 않은** 확장도 그 사실 자체를
* 산출물로 돌려줍니다(`present => false`). 미보유는 정상 상태일 수 있고, 문서는 그
* 정상 여부를 서술할 자리를 가져야 합니다.
*
* 합본은 런타임 서빙과 같은 경로(`EditorSpecAssembler`)를 씁니다. 수집기가 별도 병합
* 규칙을 갖게 되면 문서가 말하는 스펙과 편집기가 읽는 스펙이 갈라집니다.
*/
class EditorSpecCollector
{
/**
* 블록별 **항목이 실제로 담긴 자리**.
*
* 블록 최상위 키를 그대로 세면 안 됩니다 — 블록들은 자기 항목을 `entries` / `groups` /
* `byDataSourceId` 같은 하위 자리에 담고, 최상위에는 `comment` 같은 메타 키를 함께
* 둡니다. 최상위를 세면 팔레트 79개가 3(comment·groups·entries)으로 집계되는데, 그
* 숫자는 오류 없이 문서에 실려 "이 확장은 팔레트 항목이 3개" 라는 사실 주장이 됩니다.
*
* 값이 빈 배열인 블록은 최상위(메타 키 제외)가 곧 항목입니다.
*
* 선언 순서가 곧 문서 표의 행 순서입니다.
*
* @var array<string, array<int, string>> 블록 키 → 항목이 담긴 하위 키 목록
*/
private const ITEM_PATHS = [
'componentPalette' => ['entries', 'groups'],
'controls' => [],
'componentCapabilities' => [],
'nesting' => ['draggable', 'containers'],
'sampleData' => ['byDataSourceId', 'byEndpointPattern'],
'sampleGlobal' => [],
'states' => ['groups'],
'stateLabels' => [],
'actionRecipes' => [],
'conditionRecipes' => ['operators'],
'computedRecipes' => [],
'errorRecipes' => [],
'loadingComponents' => [],
'actionChipCandidates' => [],
];
/**
* 항목이 아니라 설명인 키 — 개수에서 제외한다.
*
* **접두 규칙으로 넓히지 않는다.** `_` 로 시작하는 키를 일괄 배제하면 실제 항목까지
* 삼킨다 — `sampleGlobal._local` 이 그 예이고, 그렇게 빠진 항목은 오류 없이 문서에
* "1개 적은 수" 로 실린다. 설명 키는 실측으로 확인된 것만 이름으로 열거한다.
*
* @var array<int, string> 설명 키 목록
*/
private const META_KEYS = ['comment', '$comment', '$schema', '_propControlsComment'];
/**
* @var array<int, string>|null 번들 템플릿이 커버하는 샘플 ID (프로세스 단위 메모)
*/
private static ?array $fallbackSampleIds = null;
/**
* 확장의 편집기 스펙 표면을 수집합니다.
*
* @param array<string, mixed> $record ExtensionInventory 레코드
* @param array<int, string> $layoutRelFiles 이 확장의 레이아웃 파일(확장 루트 기준 상대 경로)
* @return array<string, mixed> 편집기 스펙 인벤토리
*/
public function collect(array $record, array $layoutRelFiles = []): array
{
$manifestPath = $record['path'].DIRECTORY_SEPARATOR.'editor-spec.json';
if (! is_file($manifestPath)) {
return $this->absent($record, $layoutRelFiles);
}
$spec = EditorSpecAssembler::assemble($manifestPath);
if ($spec === null) {
// manifest 가 있는데 디코드에 실패한 상태다. "스펙 없음" 과 구분해 보고한다 —
// 뭉뚱그리면 깨진 JSON 이 "이 확장은 편집기 스펙을 두지 않는다" 로 읽힌다.
return $this->absent($record, $layoutRelFiles, malformed: true);
}
$includes = $this->includeMap($manifestPath);
return [
'present' => true,
'malformed' => false,
'manifest' => $record['relPath'].'/editor-spec.json',
'split' => $includes !== [],
'includes' => $includes,
'version' => $this->stringOrNull($spec['version'] ?? null),
'description' => $this->stringOrNull($spec['description'] ?? null),
'styleSystem' => $this->stringOrNull($spec['styleSystem'] ?? null),
'darkMode' => $this->stringOrNull(($spec['darkMode']['strategy'] ?? null)),
'blocks' => $this->blockSummaries($spec, $includes),
'paletteGroups' => $this->paletteGroups($spec['componentPalette'] ?? null),
'sampleDataIds' => $this->idsAt($spec['sampleData'] ?? null, 'byDataSourceId'),
'sampleEndpointPatterns' => $this->idsAt($spec['sampleData'] ?? null, 'byEndpointPattern'),
'stateScopes' => $this->idsAt($spec['states'] ?? null, 'groups'),
'uncovered' => $this->uncoveredDataSources($record, $layoutRelFiles, $spec),
'declaredPaths' => [
'sampleData.byDataSourceId' => $this->hasPath($spec['sampleData'] ?? null, 'byDataSourceId'),
'sampleData.byEndpointPattern' => $this->hasPath($spec['sampleData'] ?? null, 'byEndpointPattern'),
'states.groups' => $this->hasPath($spec['states'] ?? null, 'groups'),
],
];
}
/**
* 스펙 미보유(또는 손상) 상태의 산출물을 만듭니다.
*
* @param array<string, mixed> $record 확장 레코드
* @param array<int, string> $layoutRelFiles 레이아웃 파일 목록
* @param bool $malformed manifest 는 있으나 디코드에 실패했는지 여부
* @return array<string, mixed> 인벤토리
*/
private function absent(array $record, array $layoutRelFiles, bool $malformed = false): array
{
return [
'present' => false,
'malformed' => $malformed,
'manifest' => null,
'split' => false,
'includes' => [],
'version' => null,
'description' => null,
'styleSystem' => null,
'darkMode' => null,
'blocks' => [],
'paletteGroups' => [],
'sampleDataIds' => [],
'sampleEndpointPatterns' => [],
'stateScopes' => [],
'uncovered' => $this->uncoveredDataSources($record, $layoutRelFiles, []),
'declaredPaths' => [],
];
}
/**
* manifest 의 `$include` 맵을 원본 그대로 읽습니다.
*
* 합본 결과에는 `$include` 가 남지 않으므로(assemble 이 벗겨낸다) 분할 여부와 블록
* 파일 경로는 manifest 를 다시 읽어야 알 수 있습니다.
*
* @param string $manifestPath manifest 절대 경로
* @return array<string, string> 블록 키 → 상대 경로
*/
private function includeMap(string $manifestPath): array
{
$decoded = json_decode((string) file_get_contents($manifestPath), true);
if (! is_array($decoded) || ! is_array($decoded['$include'] ?? null)) {
return [];
}
$map = [];
foreach ($decoded['$include'] as $key => $relative) {
if (is_string($key) && is_string($relative) && $relative !== '') {
$map[$key] = $relative;
}
}
return $map;
}
/**
* 블록별 요약(항목 수·출처)을 만듭니다.
*
* 항목이 여러 하위 자리에 나뉜 블록(`nesting`, `sampleData`)은 자리마다 한 행을
* 냅니다. 합산하면 "끌 수 있는 컴포넌트 84 + 담을 수 있는 컨테이너 19 = 103" 처럼
* 뜻이 없는 수가 되고, 그 수는 표에서 사실처럼 읽힙니다.
*
* @param array<string, mixed> $spec 합본 spec
* @param array<string, string> $includes 블록 키 → 분할 파일 상대 경로
* @return array<int, array{key: string, count: int|null, source: string}> 블록 요약
*/
private function blockSummaries(array $spec, array $includes): array
{
$summaries = [];
foreach (self::ITEM_PATHS as $key => $paths) {
if (! array_key_exists($key, $spec)) {
continue;
}
$source = $includes[$key] ?? 'editor-spec.json (인라인)';
$block = $spec[$key];
if ($paths === []) {
$summaries[] = [
'key' => $key,
'count' => $this->countItems($block),
'source' => $source,
];
continue;
}
// 선언된 하위 자리 중 **실제로 있는 것만** 행으로 낸다. 없는 자리를 0 으로
// 내보내면 "선언했는데 비었다" 와 "그 형태를 쓰지 않는다" 가 같은 모양이 된다.
foreach ($paths as $path) {
if (! is_array($block) || ! array_key_exists($path, $block)) {
continue;
}
$summaries[] = [
'key' => $key.'.'.$path,
'count' => $this->countItems($block[$path]),
'source' => $source,
];
}
}
return $summaries;
}
/**
* 값의 항목 수를 셉니다. 맵이면 메타 키를 뺀 나머지, 리스트면 길이입니다.
*
* @param mixed $value 블록 또는 하위 자리의 값
* @return int|null 항목 수 (개수 개념이 없으면 null)
*/
private function countItems(mixed $value): ?int
{
if (! is_array($value)) {
return null;
}
if (array_is_list($value)) {
return count($value);
}
return count($this->withoutMeta($value));
}
/**
* 맵에서 메타 키를 제거합니다.
*
* @param array<string, mixed> $map 대상 맵
* @return array<string, mixed> 메타 키를 뺀 맵
*/
private function withoutMeta(array $map): array
{
return array_filter(
$map,
static fn ($k): bool => ! in_array((string) $k, self::META_KEYS, true),
ARRAY_FILTER_USE_KEY,
);
}
/**
* 팔레트 그룹별 컴포넌트 수를 셉니다.
*
* 팔레트는 `{ groups: [{ label, kind, components[] }], entries: { … } }` 형태입니다.
* `groups` 가 편집기 좌측 목록의 묶음이고, 각 묶음이 담는 컴포넌트 이름이 `components`
* 입니다.
*
* @param mixed $palette componentPalette 블록
* @return array<int, array{group: string, kind: string, count: int}> 그룹 요약
*/
private function paletteGroups(mixed $palette): array
{
if (! is_array($palette) || ! is_array($palette['groups'] ?? null)) {
return [];
}
$out = [];
foreach ($palette['groups'] as $group) {
if (! is_array($group)) {
continue;
}
$out[] = [
'group' => $this->resolveLabel($this->stringOrNull($group['label'] ?? null) ?? '(이름 없음)'),
'kind' => $this->stringOrNull($group['kind'] ?? null) ?? '-',
'count' => is_array($group['components'] ?? null) ? count($group['components']) : 0,
];
}
return $out;
}
/**
* 블록의 하위 자리에서 항목 ID 목록을 뽑습니다.
*
* 맵이면 키가 ID 이고, 리스트면 각 항목의 `id`(없으면 `scope.match`)가 ID 입니다.
* 페이지 상태는 리스트 + `scope` 형태라 키가 없습니다.
*
* @param mixed $block 블록 값
* @param string $path 항목이 담긴 하위 키
* @return array<int, string> ID 목록
*/
private function idsAt(mixed $block, string $path): array
{
if (! is_array($block) || ! is_array($block[$path] ?? null)) {
return [];
}
$items = $block[$path];
if (! array_is_list($items)) {
return array_values(array_map(
static fn ($k): string => (string) $k,
array_keys($this->withoutMeta($items)),
));
}
$ids = [];
foreach ($items as $item) {
if (! is_array($item)) {
continue;
}
$id = $this->stringOrNull($item['id'] ?? null)
?? $this->stringOrNull($item['scope']['match'] ?? null)
?? $this->stringOrNull($item['label'] ?? null);
if ($id !== null) {
$ids[] = $id;
}
}
return $ids;
}
/**
* 이 확장 레이아웃의 `data_source` 중 **프리뷰 샘플이 붙지 않는 것**을 찾습니다.
*
* 편집기 캔버스는 실제 API 를 부르지 않고 `sampleData` 로 화면을 그립니다. 그래서
* 레이아웃에 `data_source` 를 추가하고 샘플을 붙이지 않으면 그 영역만 편집기에서
* 빈 화면이 되는데, 실제 화면은 정상 동작하므로 어긋남이 드러나지 않습니다. 오류도
* 경고도 남지 않아 문서의 이 목록이 유일한 통로입니다.
*
* 커버 판정에는 확장 자신의 스펙뿐 아니라 **번들 템플릿의 스펙**도 넣습니다 —
* `settings` 처럼 여러 확장이 함께 쓰는 공용 ID 는 템플릿 스펙이 대신 채우도록 설계된
* 것이라, 그것까지 미커버로 세면 목록이 잡음으로 가득 차 정작 볼 것이 묻힙니다.
*
* 번들 템플릿은 출하 기본값입니다. 운영자가 다른 템플릿을 쓰면 커버 집합이 달라질 수
* 있으므로, 이 목록은 "반드시 빈다" 가 아니라 "기본 구성에서 빈다" 로 읽습니다.
*
* @param array<string, mixed> $record 확장 레코드
* @param array<int, string> $layoutRelFiles 레이아웃 파일(확장 루트 기준 상대 경로)
* @param array<string, mixed> $spec 이 확장의 합본 spec (없으면 빈 배열)
* @return array<int, string> 샘플이 없는 data_source ID 목록
*/
private function uncoveredDataSources(array $record, array $layoutRelFiles, array $spec): array
{
if ($layoutRelFiles === []) {
return [];
}
$covered = array_flip(array_merge($this->sampleIdsOf($spec), $this->fallbackSampleIds()));
$uncovered = [];
foreach ($layoutRelFiles as $rel) {
$abs = $record['path'].DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $rel);
$layout = $this->decodeJson($abs);
if ($layout === null) {
continue;
}
foreach ($this->layoutDataSourceIds($layout) as $id) {
if (! isset($covered[$id])) {
$uncovered[$id] = true;
}
}
}
$ids = array_keys($uncovered);
sort($ids);
return $ids;
}
/**
* spec 의 `sampleData.byDataSourceId` 키 목록을 뽑습니다.
*
* @param array<string, mixed> $spec 합본 spec
* @return array<int, string> ID 목록
*/
private function sampleIdsOf(array $spec): array
{
return $this->idsAt($spec['sampleData'] ?? null, 'byDataSourceId');
}
/**
* 번들 템플릿 스펙이 채우는 샘플 ID 집합을 돌려줍니다.
*
* 결과를 프로세스 단위로 기억합니다. 확장 20개를 도는 동안 매번 다시 합본하면 템플릿
* 스펙(팔레트·컨트롤·역량을 담아 수만 줄에 이른다)을 스무 번 메모리에 올리게 되고,
* 메모리 한도가 낮은 실행 환경(테스트 프로세스)에서는 그대로 OOM 이 됩니다. 남기는
* 것은 스펙 전체가 아니라 **ID 문자열 목록**이라 유지 비용도 작습니다.
*
* @return array<int, string> 번들 템플릿이 커버하는 data_source ID 목록
*/
private function fallbackSampleIds(): array
{
if (self::$fallbackSampleIds !== null) {
return self::$fallbackSampleIds;
}
$glob = glob(base_path('templates'.DIRECTORY_SEPARATOR.'_bundled'.DIRECTORY_SEPARATOR.'*'.DIRECTORY_SEPARATOR.'editor-spec.json'));
$ids = [];
foreach ($glob === false ? [] : $glob as $manifest) {
$assembled = EditorSpecAssembler::assemble($manifest);
if (is_array($assembled)) {
$ids = array_merge($ids, $this->sampleIdsOf($assembled));
}
// 합본 결과를 즉시 놓아준다 — 다음 manifest 를 읽기 전에 회수되게 한다.
unset($assembled);
}
return self::$fallbackSampleIds = array_values(array_unique($ids));
}
/**
* 레이아웃 JSON 트리에서 `data_sources[].id` 를 전부 긁습니다.
*
* `data_sources` 는 최상위뿐 아니라 컴포넌트 노드에도 붙을 수 있어 트리 전체를 봅니다.
*
* @param array<string, mixed> $node 레이아웃 노드
* @return array<int, string> data_source ID 목록
*/
private function layoutDataSourceIds(array $node): array
{
$ids = [];
if (is_array($node['data_sources'] ?? null)) {
foreach ($node['data_sources'] as $ds) {
$id = is_array($ds) ? $this->stringOrNull($ds['id'] ?? null) : null;
if ($id !== null) {
$ids[] = $id;
}
}
}
foreach ($node as $value) {
if (is_array($value)) {
$ids = array_merge($ids, $this->layoutDataSourceIds($value));
}
}
return $ids;
}
/**
* `$t:` 접두 다국어 키를 코어 한국어 문자열로 풉니다.
*
* 편집기 스펙의 라벨은 화면에 그대로 나오지 않고 프론트 i18n 을 거칩니다. 문서에 키
* 원문(`$t:layout_editor.palette.group.design`)을 그대로 실으면 읽는 쪽이 그 묶음이
* 무엇인지 알 수 없습니다.
*
* Laravel 의 `__()` 로는 풀리지 않습니다 — 이 키들은 PHP lang 파일이 아니라 프론트
* 다국어 JSON(`lang/ko.json` + `$partial` 분할)에 있고, JSON 번역기는 문자열 전체를
* 키로 쓰기 때문입니다. 그래서 `$partial` 한 홉을 직접 따라갑니다.
*
* 풀리지 않으면 **원문을 그대로 둡니다.** 없는 번역을 지어내는 것보다 키가 드러나는
* 편이 어디를 고쳐야 하는지 알려 주고, 이 해석이 어긋나도 문서가 틀린 사실을 주장하는
* 대신 키만 남습니다.
*
* @param string $label 라벨 원문
* @return string 해석된 라벨 (실패 시 원문)
*/
private function resolveLabel(string $label): string
{
if (! str_starts_with($label, '$t:')) {
return $label;
}
$root = base_path('lang');
$node = $this->langRoot($root);
if ($node === null) {
return $label;
}
foreach (explode('.', substr($label, 3)) as $segment) {
if (is_array($node) && isset($node['$partial']) && is_string($node['$partial'])) {
$node = $this->decodeJson($root.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $node['$partial']));
}
if (! is_array($node) || ! array_key_exists($segment, $node)) {
return $label;
}
$node = $node[$segment];
}
return is_string($node) && $node !== '' ? $node : $label;
}
/**
* 코어 프론트 다국어 루트(`lang/ko.json`)를 읽습니다.
*
* @param string $root `lang` 디렉토리 절대 경로
* @return array<string, mixed>|null 디코드 결과
*/
private function langRoot(string $root): ?array
{
return $this->decodeJson($root.DIRECTORY_SEPARATOR.'ko.json');
}
/**
* JSON 파일을 배열로 읽습니다.
*
* @param string $path 절대 경로
* @return array<string, mixed>|null 디코드 결과 (실패 시 null)
*/
private function decodeJson(string $path): ?array
{
if (! is_file($path)) {
return null;
}
$decoded = json_decode((string) file_get_contents($path), true);
return is_array($decoded) ? $decoded : null;
}
/**
* 블록이 그 하위 자리를 **선언했는지** 봅니다.
*
* 선언하고 비운 것(`0`)과 아예 선언하지 않은 것은 다릅니다 — 전자는 채울 자리가
* 있다는 뜻이고 후자는 그 형태를 쓰지 않는다는 뜻입니다. 뭉뚱그려 `0` 으로 적으면
* 읽는 쪽이 "채워야 할 자리를 비워 뒀다" 로 오해합니다.
*
* @param mixed $block 블록 값
* @param string $path 하위 키
* @return bool 선언 여부
*/
private function hasPath(mixed $block, string $path): bool
{
return is_array($block) && array_key_exists($path, $block);
}
/**
* 비어 있지 않은 문자열만 통과시킵니다.
*
* @param mixed $value 후보 값
* @return string|null 문자열 또는 null
*/
private function stringOrNull(mixed $value): ?string
{
return is_string($value) && $value !== '' ? $value : null;
}
}
@@ -0,0 +1,72 @@
<?php
namespace App\Support\ExtensionDoc;
/**
* 확장 문서 수집 컨텍스트 조립기
*
* 문서 블록을 렌더하려면 수집기 여덟 축(선언형 표면·훅·데이터 모델·프론트·테스트·의존
* 관계·편집기 스펙)의 산출물을 한 배열로 묶어야 합니다. 그 조립을 커맨드와 계약 테스트가
* 각자 하고 있으면, 축이 하나 늘 때 한쪽만 갱신되어 **드리프트가 아닌 드리프트**가 보고
* 됩니다 — 테스트가 만든 블록에는 그 축이 없어 파일과 달라지기 때문입니다.
*
* 그 어긋남은 "생성기를 다시 돌려라" 라는 메시지로 나타나는데, 아무리 돌려도 사라지지
* 않습니다. 조립을 이 한 곳이 소유해 그 경로 자체를 없앱니다.
*/
class ExtensionDocContext
{
/**
* 수집기를 주입받습니다.
*
* @param DeclarativeSurfaceCollector $surface 선언형 표면 수집기
* @param HookInventory $hooks 훅 인벤토리
* @param DataModelCollector $data 데이터 모델 수집기
* @param FrontendInventory $frontend 프론트 인벤토리
* @param TestPathCollector $tests 테스트 경로 수집기
* @param DependencyGraphCollector $deps 의존 관계 수집기
* @param EditorSpecCollector $editorSpec 편집기 스펙 수집기
*/
public function __construct(
private readonly DeclarativeSurfaceCollector $surface,
private readonly HookInventory $hooks,
private readonly DataModelCollector $data,
private readonly FrontendInventory $frontend,
private readonly TestPathCollector $tests,
private readonly DependencyGraphCollector $deps,
private readonly EditorSpecCollector $editorSpec,
) {}
/**
* 확장 하나의 수집 컨텍스트를 조립합니다.
*
* @param array<string, mixed> $record ExtensionInventory 레코드
* @return array<string, mixed> 수집 컨텍스트
*/
public function build(array $record): array
{
// 선언형 표면을 먼저 모은다 — 발행 훅의 1차 출처가 그 안의 `getHooks()` 선언이다.
$surface = $this->surface->collect($record);
$declaredHooks = $surface['values']['getHooks'] ?? [];
// 편집기 스펙 수집기는 레이아웃 목록을 받아 "프리뷰 샘플이 없는 data_source" 를
// 판정한다. 레이아웃 경로 규약(모듈·플러그인 `resources/layouts/` ↔ 템플릿
// `layouts/`)은 FrontendInventory 가 단독으로 소유하므로, 그 결과를 넘겨 분기가
// 두 곳에 생기지 않게 한다.
$frontend = $this->frontend->collect($record);
$layoutRelFiles = array_map(
static fn (array $layout): string => $layout['relFile'],
$frontend['layouts'],
);
return [
'record' => $record,
'surface' => $surface,
'hooks' => $this->hooks->collect($record, is_array($declaredHooks) ? $declaredHooks : []),
'data' => $this->data->collect($record),
'frontend' => $frontend,
'tests' => $this->tests->collect($record),
'deps' => $this->deps->collect($record),
'editorSpec' => $this->editorSpec->collect($record, $layoutRelFiles),
];
}
}
@@ -21,6 +21,11 @@ class ExtensionDocScaffolder
*/
public const GEN_PREFIX = '<!-- @generated:';
/**
* @var string 미채움 축 라벨 — 마커를 지우고 서술을 쓰지 않은 빈 `@intent` 블록
*/
public const EMPTY_INTENT_LABEL = '(빈 서술)';
/**
* @var string 미채움 마커 — 설계 의도 / 소개 서술
*/
@@ -166,6 +171,24 @@ class ExtensionDocScaffolder
'부트스트랩' => 'frontend-entry',
],
],
// 편집기 스펙은 **세 유형 모두** 가진다. 스펙을 두지 않은 확장에도 문서를 두는 것은
// 미보유가 곧 정상일 수 있기 때문이다 — "이 확장은 왜 편집기 스펙이 없어도 되는가 /
// 언제 필요해지는가" 를 적을 자리가 없으면, 다음 사람이 그 부재를 누락으로 오해하거나
// 반대로 필요한 시점을 놓친다. 미보유 확장의 블록은 그 사실을 명시하고 사람 서술로
// 이어진다.
'docs/editor-spec.md' => [
'types' => ['module', 'plugin', 'template'],
// 절 ↔ 블록은 **키로** 묶는다. 순번 결합은 절을 하나 끼우는 순간 그 뒤 블록이
// 전부 엉뚱한 헤딩 밑으로 들어가는데, 헤딩 존재·블록 존재를 각각만 보는 게이트는
// 그 어긋남을 잡지 못한다. 필수 절 목록은 이 배열의 키에서 파생한다(중복 정의 없음).
'blocks' => [
'선언 요약' => 'editor-spec-summary',
'선언 블록' => 'editor-spec-blocks',
'컴포넌트 팔레트' => 'editor-spec-palette',
'샘플 데이터와 페이지 상태' => 'editor-spec-samples',
'수정 시 동반 의무' => 'editor-spec-obligations',
],
],
];
/**
@@ -347,6 +370,35 @@ class ExtensionDocScaffolder
];
}
/**
* 본문이 빈 사람 영역(`@intent`) 블록 수를 셉니다.
*
* 미채움은 `TODO:` 마커로만 드러나지 않습니다. 스캐폴딩 직후 마커를 **지우기만 하고**
* 서술을 쓰지 않으면 마커 검사는 0 을 돌려주고 골격·블록 검사도 전부 통과합니다 —
* 결과가 "다 채웠다" 와 구분되지 않아, 비어 있는 문서가 완비로 집계됩니다.
*
* 그래서 마커 잔량과 빈 본문을 **같은 축**(미채움)으로 함께 봅니다.
*
* @param string $content 문서 전문
* @return int 빈 `@intent` 블록 수
*/
public static function emptyIntentBlocks(string $content): int
{
if (preg_match_all('/<!-- @intent START -->(.*?)<!-- @intent END -->/s', $content, $m) === false) {
return 0;
}
$empty = 0;
foreach ($m[1] as $body) {
if (trim($body) === '') {
$empty++;
}
}
return $empty;
}
/**
* 자동 생성 블록을 마커로 감쌉니다.
*
@@ -565,6 +617,11 @@ class ExtensionDocScaffolder
'assets' => $this->renderAssets($ctx),
'components' => $this->renderComponents($ctx),
'layout-map' => $this->renderLayoutMap($ctx),
'editor-spec-summary' => $this->renderEditorSpecSummary($ctx),
'editor-spec-blocks' => $this->renderEditorSpecBlocks($ctx),
'editor-spec-palette' => $this->renderEditorSpecPalette($ctx),
'editor-spec-samples' => $this->renderEditorSpecSamples($ctx),
'editor-spec-obligations' => $this->renderEditorSpecObligations($ctx),
default => $this->none('알 수 없는 블록 키: '.$key),
};
}
@@ -593,7 +650,7 @@ class ExtensionDocScaffolder
$core = $ctx['deps']['coreVersion'] ?? null;
if ($core !== null) {
$badges[] = $this->badge('G7', $core, '1F883D');
$badges[] = $this->badge('그누보드7', $core, '1F883D');
}
$license = $manifest['license'] ?? null;
@@ -639,7 +696,7 @@ class ExtensionDocScaffolder
private function renderRequirements(array $ctx): string
{
$rows = [];
$rows[] = ['G7 코어', $this->code($ctx['deps']['coreVersion'] ?? '(제약 없음)')];
$rows[] = ['그누보드7 코어', $this->code($ctx['deps']['coreVersion'] ?? '(제약 없음)')];
$rows[] = ['PHP', $this->code($this->composerPhp($ctx) ?? '^8.2')];
foreach ($ctx['deps']['requires'] as $dep) {
@@ -826,6 +883,7 @@ class ExtensionDocScaffolder
'docs/components.md' => '템플릿이 제공하는 컴포넌트',
'docs/layouts.md' => '레이아웃 목록과 라우트 매핑',
'docs/handlers.md' => '템플릿 전용 핸들러와 부트스트랩',
'docs/editor-spec.md' => '레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터',
default => '-',
};
}
@@ -2252,7 +2310,7 @@ class ExtensionDocScaffolder
$lines = [];
$lines[] = '# '.$name;
$lines[] = '';
$lines[] = "**G7 {$label} · {$record['id']}**";
$lines[] = "**그누보드7 {$label} · {$record['id']}**";
$lines[] = $this->escape($description);
$lines[] = '';
$lines[] = self::wrap('badges', $this->renderBlock('badges', $ctx));
@@ -2455,10 +2513,284 @@ class ExtensionDocScaffolder
'docs/components.md' => '컴포넌트',
'docs/layouts.md' => '레이아웃',
'docs/handlers.md' => '핸들러',
'docs/editor-spec.md' => '레이아웃 편집기 스펙',
default => basename($doc, '.md'),
};
}
// -----------------------------------------------------------------------
// 편집기 스펙 블록 렌더러
// -----------------------------------------------------------------------
/**
* 편집기 스펙 보유 여부와 형태를 렌더합니다.
*
* 미보유는 결함이 아니라 정상일 수 있으므로 "없음" 을 사실로 적고 사람 서술에 넘깁니다.
* 다만 manifest 가 있는데 읽지 못한 경우(malformed)는 구분해 적습니다 — 뭉뚱그리면
* 깨진 JSON 이 "이 확장은 편집기 스펙을 두지 않는다" 로 굳습니다.
*
* @param array<string, mixed> $ctx 수집 컨텍스트
* @return string 마크다운
*/
private function renderEditorSpecSummary(array $ctx): string
{
$spec = $ctx['editorSpec'] ?? null;
if (! is_array($spec)) {
return $this->none('편집기 스펙을 수집하지 못했습니다.');
}
if (($spec['malformed'] ?? false) === true) {
return $this->none('`editor-spec.json` 이 존재하지만 JSON 으로 읽지 못했습니다 — 편집기가 이 확장의 선언을 무시하고 있습니다.');
}
if (($spec['present'] ?? false) !== true) {
return $this->none('이 확장은 편집기 스펙(`editor-spec.json`)을 두지 않습니다. 편집기는 코어 기본 팔레트와 활성 템플릿의 스펙만으로 이 확장의 화면을 다룹니다.');
}
$rows = [];
$rows[] = ['manifest', $this->code((string) $spec['manifest'])];
$rows[] = ['형태', $spec['split'] === true
? '분할 — manifest + `editor-spec/*.json` '.count($spec['includes']).'개 블록'
: '단일 파일 (인라인)'];
$rows[] = ['스펙 버전', $this->code((string) ($spec['version'] ?? ''))];
$rows[] = ['스타일 시스템', $this->code((string) ($spec['styleSystem'] ?? ''))];
$rows[] = ['다크 모드 전략', $this->code((string) ($spec['darkMode'] ?? ''))];
$table = $this->table(['항목', '값'], $rows);
$description = $spec['description'] ?? null;
if (is_string($description) && $description !== '') {
return $table."\n\n> ".$this->escape($description);
}
return $table;
}
/**
* 스펙이 선언한 블록별 항목 수를 렌더합니다.
*
* @param array<string, mixed> $ctx 수집 컨텍스트
* @return string 마크다운
*/
private function renderEditorSpecBlocks(array $ctx): string
{
$spec = $ctx['editorSpec'] ?? null;
if (! is_array($spec) || ($spec['present'] ?? false) !== true) {
return $this->none('선언된 편집기 스펙 블록이 없습니다.');
}
$rows = [];
foreach ($spec['blocks'] as $block) {
$rows[] = [
$this->code($block['key']),
$this->editorSpecBlockRole($block['key']),
$block['count'] === null ? '-' : (string) $block['count'],
$this->code($block['source']),
];
}
return $this->table(['블록', '역할', '항목 수', '출처'], $rows);
}
/**
* 편집기 스펙 블록의 역할을 한 줄로 설명합니다.
*
* 블록 키는 코어가 정한 어휘이므로 확장마다 다시 설명할 필요가 없고, 반대로 설명이
* 없으면 확장 문서만 읽는 쪽이 키 이름만으로 의미를 추측하게 됩니다.
*
* @param string $key 블록 키
* @return string 역할 설명
*/
private function editorSpecBlockRole(string $key): string
{
return match ($key) {
'componentPalette', 'componentPalette.entries' => '편집기 "요소 추가" 팔레트에 나타나는 항목',
'componentPalette.groups' => '팔레트 좌측 목록의 묶음',
'nesting.draggable' => '캔버스에서 끌어 옮길 수 있는 컴포넌트',
'nesting.containers' => '자식을 담을 수 있는 컴포넌트와 그 허용 규칙',
'sampleData.byDataSourceId' => '레이아웃 `data_sources` ID 로 붙는 프리뷰 응답',
'sampleData.byEndpointPattern' => '엔드포인트 패턴으로 붙는 프리뷰 응답',
'states.groups' => '상태 변종을 적용할 범위(라우트·베이스 레이아웃)',
'conditionRecipes.operators' => '조건 표현식에 쓸 수 있는 연산자',
'controls' => '재사용 스타일 컨트롤 정의',
'componentCapabilities' => '컴포넌트별 편집 역량(어떤 속성을 편집기가 다루는가)',
'nesting' => '어떤 컴포넌트 안에 무엇을 넣을 수 있는가',
'sampleData' => '캔버스 프리뷰용 샘플 응답',
'sampleGlobal' => '`_global.*` 프리뷰 baseline 시드',
'states' => '페이지 상태 변종(빈 목록·오류 등)',
'stateLabels' => '상태값 친화 명칭 카탈로그',
'actionRecipes' => '친화 명칭 → 액션 JSON 레시피',
'conditionRecipes' => '친화 조건 → `if` 표현식 레시피',
'computedRecipes' => '계산값 레시피',
'errorRecipes' => '오류 처리 레시피',
'loadingComponents' => '로딩 표시 컴포넌트 후보',
'actionChipCandidates' => '동작 데이터 칩 컨텍스트 후보',
default => '-',
};
}
/**
* ID 목록을 표 한 칸에 담을 수 있게 줄입니다.
*
* 70개를 한 줄로 늘어놓으면 표가 읽히지 않고, 그렇다고 개수만 남기면 어떤 ID 인지
* 확인할 길이 사라집니다. 앞쪽을 보이고 나머지는 수로 줄입니다.
*
* @param array<int, string> $ids ID 목록
* @param int $limit 나열할 최대 개수
* @return string 마크다운 셀 내용
*/
private function idListCell(array $ids, int $limit = 12): string
{
if ($ids === []) {
return '-';
}
$shown = array_slice($ids, 0, $limit);
$cell = implode(' · ', array_map(fn (string $id): string => $this->code($id), $shown));
$rest = count($ids) - count($shown);
return $rest > 0 ? $cell." … 외 {$rest}개" : $cell;
}
/**
* 팔레트 그룹별 항목 수를 렌더합니다.
*
* @param array<string, mixed> $ctx 수집 컨텍스트
* @return string 마크다운
*/
private function renderEditorSpecPalette(array $ctx): string
{
$spec = $ctx['editorSpec'] ?? null;
if (! is_array($spec) || ($spec['present'] ?? false) !== true) {
return $this->none('이 확장은 편집기 팔레트에 항목을 추가하지 않습니다.');
}
$rows = [];
foreach ($spec['paletteGroups'] as $group) {
$rows[] = [
$this->escape($group['group']),
$this->code($group['kind']),
(string) $group['count'],
];
}
if ($rows === []) {
return $this->none('이 확장은 `componentPalette` 를 선언하지 않습니다 — 편집기 팔레트에 추가되는 항목이 없습니다.');
}
return $this->table(['그룹', '종류', '컴포넌트 수'], $rows);
}
/**
* 샘플 데이터 ID 와 페이지 상태 변종을 렌더합니다.
*
* 이 두 축은 편집기 캔버스가 실제 API 없이 화면을 그릴 때 쓰는 값입니다. 레이아웃의
* `data_sources` ID 와 어긋나면 편집기 프리뷰만 빈 화면이 되는데, 실제 화면은 정상이라
* 어긋남이 드러나지 않습니다.
*
* @param array<string, mixed> $ctx 수집 컨텍스트
* @return string 마크다운
*/
private function renderEditorSpecSamples(array $ctx): string
{
$spec = $ctx['editorSpec'] ?? null;
if (! is_array($spec)) {
return $this->none('편집기 스펙을 수집하지 못했습니다.');
}
// 스펙이 없어도 **미커버 목록은 낸다.** 스펙이 없는 확장이야말로 프리뷰가 비는
// 자리를 가질 가능성이 크고, 여기서 조기 반환하면 정작 필요한 확장에서 그 목록이
// 통째로 사라진다 — 그런데 결과는 "빈 자리가 없다" 와 구분되지 않는다.
$sections = [];
if (($spec['present'] ?? false) === true) {
// 세 자리를 한 행으로 합치지 않는다 — `data_sources` ID 로 붙는 샘플과 엔드포인트
// 패턴으로 붙는 샘플은 어긋났을 때 고칠 자리가 다르고, 페이지 상태는 ID 가 아니라
// 적용 범위(라우트/베이스 레이아웃)로 식별된다.
$rows = [];
foreach ([
['sampleData.byDataSourceId', $spec['sampleDataIds']],
['sampleData.byEndpointPattern', $spec['sampleEndpointPatterns']],
['states.groups', $spec['stateScopes']],
] as [$label, $ids]) {
$declared = ($spec['declaredPaths'][$label] ?? false) === true;
$rows[] = [
$this->code($label),
$this->editorSpecBlockRole($label),
$declared ? (string) count($ids) : '미선언',
$declared ? $this->idListCell($ids) : '-',
];
}
$sections[] = $this->table(['자리', '역할', '개수', 'ID'], $rows);
} else {
$sections[] = $this->none('이 확장은 편집기 스펙을 두지 않아 선언된 샘플 데이터·페이지 상태가 없습니다.');
}
$uncovered = $spec['uncovered'] ?? [];
if ($uncovered === []) {
$sections[] = $this->none('이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버).');
} else {
$sections[] = '**프리뷰 샘플이 없는 `data_source` '.count($uncovered).'개** — 편집기 캔버스에서 이 자리만 빈 화면이 됩니다. 실제 화면은 정상이라 오류도 경고도 남지 않습니다.'
."\n\n".$this->idListCell($uncovered, 20);
}
return implode("\n\n", $sections);
}
/**
* 편집기 스펙을 함께 고쳐야 하는 변경과 그 절차를 렌더합니다.
*
* 이 표가 없으면 확장에 화면 요소를 추가해도 편집기 팔레트에 나타나지 않는 상태가
* 오류도 경고도 없이 남습니다 — 편집기는 선언되지 않은 컴포넌트를 "없는 것" 으로
* 다룰 뿐 실패를 보고하지 않습니다.
*
* @param array<string, mixed> $ctx 수집 컨텍스트
* @return string 마크다운
*/
private function renderEditorSpecObligations(array $ctx): string
{
$record = $ctx['record'];
$spec = $ctx['editorSpec'] ?? null;
$hasSpec = is_array($spec) && ($spec['present'] ?? false) === true;
$update = "php artisan {$record['type']}:update {$record['id']} --force";
$rows = [
['컴포넌트를 새로 만들었다', '`componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정'],
['레이아웃에 `data_sources` 를 추가했다', '`sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면)'],
['`_global.*` 을 새로 읽는다', '`sampleGlobal` 에 baseline 값 추가'],
['빈 목록·오류 같은 화면 변종을 추가했다', '`states` 에 변종 추가 · `stateLabels` 에 친화 명칭'],
['새 액션·조건 패턴을 도입했다', '`actionRecipes` / `conditionRecipes` 에 친화 명칭 등록'],
];
$table = $this->table(['이런 변경을 했다면', '편집기 스펙에서 함께 할 일'], $rows);
if (! $hasSpec) {
// 스펙이 없는 확장에도 이 표를 남긴다. 지금은 해당 없음이지만, 위 사건 중 하나가
// 일어나는 순간 스펙을 **신설**해야 한다는 것이 이 문서가 전할 내용이다.
return $this->none('이 확장은 아직 편집기 스펙을 두지 않습니다. 아래 변경이 생기면 `editor-spec.json` 을 신설합니다.')
."\n\n".$table;
}
// `_bundled` 편집분은 update 커맨드로 활성 디렉토리에 반영된 뒤에만 편집기에 보인다
// (`EditorSpecAssembler` 는 활성 디렉토리만 합본한다 — `_bundled` 폴백이 없다).
return $table."\n\n"
.'편집기 스펙은 JSON 이므로 빌드가 필요 없습니다. 다만 편집기 서빙은 **활성 디렉토리만** 읽으므로(`_bundled` 폴백 없음) 편집 후 반드시 반영합니다:'
."\n\n```bash\n".$update."\n```";
}
// -----------------------------------------------------------------------
// 마크다운 유틸
// -----------------------------------------------------------------------
+68
View File
@@ -10,6 +10,7 @@
3. 분할 형식: manifest(editor-spec.json) + `$include` 맵 → editor-spec/{block}.json. 서버가 합본해 단일 spec 으로 서빙
4. 서빙은 활성 디렉토리만 기준 (_bundled 폴백 없음). _bundled 작업분은 {type}:update 로 활성 반영 후 런타임 노출
5. 친화 라벨은 $t: 다국어 키, comment 는 작성자 메모(엔진 무시), 알려진 필드 외 자유 필드는 보존만
6. 확장별 선언 실측·동반 의무는 그 확장의 docs/editor-spec.md 가 소유 (ext:docgen 이 유지)
```
## 개요
@@ -60,6 +61,71 @@ editor-spec.json 이 커지면(코어 admin 템플릿은 단일 파일 18,000줄
런타임 서빙은 **활성 디렉토리(`templates/{id}/...`)만** 읽는다. `_bundled` 폴백은 없다. `_bundled` 에서 작업한 분할본은 `{type}:update {id} --force` 로 활성 디렉토리에 반영된 뒤에만 편집기에 나타난다. JSON 만 바뀐 경우 빌드는 불필요하고 update 만 실행한다.
## 확장 개발자의 작업 순서
아래 규칙들은 블록 하나하나의 **문법**을 말한다. 실제로 확장을 고칠 때 필요한 것은 그
앞의 판단이다 — 무엇을 손대야 하고, 무엇을 빠뜨리면 어떤 증상이 나는가.
### 어느 확장의 스펙에 넣는가
컴포넌트를 만드는 것은 템플릿의 일이고, 모듈·플러그인은 템플릿이 제공하는 컴포넌트를
쓰기만 한다. 그래서 두 부류가 담는 것이 다르다.
| 선언할 것 | 자리 |
|---|---|
| `componentPalette` · `controls` · `componentCapabilities` · `nesting` | **템플릿** 스펙 |
| `sampleData` · `sampleGlobal` · `states` · `*Recipes` | 그 데이터를 소유한 **모듈·플러그인** 스펙 |
| 여러 확장이 함께 쓰는 공용 ID(`settings` · `roles` · `me` 등) | **템플릿** 스펙 |
공용 ID 를 확장마다 각자 선언하면 같은 ID 의 샘플이 여러 곳에 생기고, 그것들이 갈라져도
오류가 나지 않는다 — 어느 것이 쓰이는지가 병합 순서에 좌우된다.
모듈·플러그인이 `componentPalette` 를 선언하면 템플릿 선언과 같은 자리를 두고 다투게
되므로 두지 않는다. 팔레트에 얹고 싶은 것이 있다면 그것은 활성 템플릿의 스펙으로 간다.
### 무엇을 빠뜨렸는지는 증상으로 가른다
편집기는 선언되지 않은 것을 **없는 것**으로 다룰 뿐 실패를 보고하지 않는다. 그래서
빠뜨린 자리를 알려 주는 것은 오류 메시지가 아니라 증상이다.
| 증상 | 빠뜨린 자리 |
|---|---|
| 컴포넌트가 팔레트 목록에 아예 없다 | `componentPalette.entries` |
| 팔레트에 등록했는데 목록에 안 보인다 | `componentPalette.groups` 의 어느 묶음에도 없다 |
| 팔레트에서 끌어 놓을 수 없다 | `nesting.draggable` |
| 놓을 자리가 없다(컨테이너가 거부한다) | `nesting.containers` |
| 놓았는데 속성 패널이 비어 있다 | `componentCapabilities` |
| 속성 패널에 특정 항목만 없다 | `controls` |
| 캔버스의 한 영역만 빈 화면이다 | `sampleData.byDataSourceId`(또는 `byEndpointPattern`) |
| 값이 `undefined` 라 영역 전체가 사라진다 | `sampleGlobal` |
| 특정 상태(빈 목록·오류·모달 열림)를 볼 수 없다 | `states` |
캔버스는 실제 API 를 부르지 않고 `sampleData` 로 그린다. 그래서 레이아웃에
`data_source` 를 추가하고 샘플을 붙이지 않으면 **편집기에서만** 그 자리가 비고 실제
화면은 정상 동작한다 — 오류도 경고도 서버 로그도 남지 않는다.
캔버스는 또한 정적 시뮬레이션이라 클릭·응답으로 만들어지는 상태를 스스로 만들지 못한다.
모바일 드로어(햄버거 클릭), 쿠키 동의 배너(동의 전 방문자), 본인인증 창(428 응답)처럼
**어떤 사건 뒤에만 나타나는 화면**은 `states` 로 그 상태를 주입해 두지 않으면 편집
자체가 불가능하다.
### 반영 절차
편집기 스펙은 JSON 이므로 빌드가 필요 없다. 다만 서빙은 **활성 디렉토리만** 읽고
`_bundled` 폴백이 없으므로, `_bundled` 에서 고친 뒤 update 커맨드를 돌리지 않으면
편집기에는 직전 내용이 그대로 보인다. 파일은 고쳤는데 화면이 안 바뀌었다면 거의 이 경우다.
```bash
php artisan {module|plugin|template}:update {id} --force
```
### 확장별 실측은 그 확장이 소유한다
어느 확장이 무엇을 얼마나 선언했는지, 그 확장에서 프리뷰 샘플이 붙지 않는 `data_source`
가 무엇인지는 **그 확장의 `docs/editor-spec.md`** 가 답한다. `php artisan ext:docgen` 이
그 실측 부분을 유지하므로 스펙을 고친 뒤 재실행한다. 코어 문서(이 문서)에는 블록 문법과
공통 판단 기준만 둔다.
## 블록별 작성 규칙
### componentPalette — 요소 추가 팔레트
@@ -240,4 +306,6 @@ export function registerSirsoftAdminBasicEditorWidgets(): void {
- 타입 SSoT: `resources/js/core/template-engine/layout-editor/spec/specTypes.ts`
- 로더(fetch + 병합): `resources/js/core/template-engine/layout-editor/spec/editorSpecLoader.ts`
- 서버 합본 헬퍼: `app/Extension/Helpers/EditorSpecAssembler.php`
- 확장별 실측 문서 생성: `php artisan ext:docgen` — 각 확장 `docs/editor-spec.md`
- 확장 문서 규정: [extension-documentation.md](extension-documentation.md)
- 서빙: `app/Http/Controllers/Api/Public/PublicTemplateController.php`, `app/Http/Controllers/Api/Admin/AdminTemplateAssetController.php`, `app/Services/ModuleService.php`, `app/Services/PluginService.php`
@@ -54,11 +54,14 @@
├─ components.md 제공 컴포넌트 [템플릿]
├─ layouts.md 레이아웃 목록 · 라우트 매핑 [템플릿]
├─ handlers.md 템플릿 전용 핸들러 · 부트스트랩 [템플릿]
├─ editor-spec.md 레이아웃 편집기 선언 · 프리뷰 샘플 커버리지
└─ api/ API 레퍼런스 (별도 체계)
```
템플릿은 API·모델·훅 축이 사실상 비므로 골격이 다르다. `ext:docgen` 이 manifest 유형을 보고 골격을 고르므로 유형을 직접 지정할 필요가 없다.
`docs/editor-spec.md` 만은 **세 유형 공통**이다. 편집기 스펙(`editor-spec.json`)을 두지 않는 확장에도 문서를 두는 것은, 미보유가 정상일 수 있고 그 정상 여부를 적을 자리가 필요하기 때문이다 — "이 확장은 왜 스펙이 없어도 되는가 / 언제 필요해지는가" 가 어디에도 없으면 다음 사람이 그 부재를 누락으로 오해하거나 반대로 필요한 시점을 놓친다.
이 문서들은 `{ext}/` 안에 있으므로 **릴리즈 페이로드에 실리는 공개 배포물**이다. 확장 저장소를 받은 사람이 그대로 읽는다.
## 3. 역할 경계와 SSoT
@@ -93,6 +96,7 @@
| `docs/components.md` (템플릿) | `제공 컴포넌트` |
| `docs/layouts.md` (템플릿) | `레이아웃 목록` · `라우트 매핑` |
| `docs/handlers.md` (템플릿) | `템플릿 전용 핸들러` · `부트스트랩` |
| `docs/editor-spec.md` | `선언 요약` · `선언 블록` · `컴포넌트 팔레트` · `샘플 데이터와 페이지 상태` · `수정 시 동반 의무` |
목록의 SSoT 는 생성기이므로, 직접 옮겨 적기보다 `ext:docgen --init` 이 만든 골격에서 시작하는 편이 어긋나지 않는다.
@@ -71,7 +71,7 @@ Listener · Layout · Test · 다국어(백엔드 PHP + 프론트 JSON)가 각 1
`Memo` 모델. 저장 직후 `gnuboard7-hello_module.memo.created` 액션 훅을 발행하고,
`LogMemoCreatedListener` 가 그것을 받아 로그를 남깁니다.
이 한 흐름에 G7 모듈의 규약이 전부 들어 있습니다:
이 한 흐름에 그누보드7 모듈의 규약이 전부 들어 있습니다:
- 검증은 Service 가 아니라 **FormRequest** 에 둔다
- Service 는 구체 Repository 가 아니라 **인터페이스**를 주입받는다
@@ -129,6 +129,7 @@ Listener · Layout · Test · 다국어(백엔드 PHP + 프론트 JSON)가 각 1
- [ ] 규약(FormRequest 검증 · Repository 인터페이스 주입 · 훅으로 부가작업 분리)이 흐트러지지 않았는지 확인 — 이 코드는 본보기로 읽힌다
- [ ] `docs/extension/sample-extensions.md` 의 계층 표와 어긋나지 않는지 확인 (파일을 추가·삭제했다면 그 표도 갱신)
- [ ] 발행 훅 이름을 바꾸면 `gnuboard7-hello_plugin` 의 구독이 조용히 끊긴다
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 를 확인 — 이 확장은 편집기 스펙이 없어 `memoData` · `memos` 가 편집기 캔버스에서 빈 화면으로 보인다. `data_source` 를 더 늘리면 그 자리도 같은 상태가 된다
## 6. 금지 패턴
@@ -176,6 +177,7 @@ php vendor/bin/phpunit modules/_bundled/gnuboard7-hello_module/tests --filter='<
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [0.1.1] - 2026-08-10
@@ -1,13 +1,13 @@
# Hello 모듈
**G7 모듈 · gnuboard7-hello_module**
**그누보드7 모듈 · gnuboard7-hello_module**
학습용 최소 샘플 모듈 (Memo CRUD)
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-0.1.2-0066FF?style=flat-square" alt="version 0.1.2">
<img src="https://img.shields.io/badge/type-%EB%AA%A8%EB%93%88-555555?style=flat-square" alt="type 모듈">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.0-1F883D?style=flat-square" alt="G7 &gt;=7.0.0">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.0-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.0">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
</p>
<!-- @generated:badges END -->
@@ -70,7 +70,7 @@ flowchart LR
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.0` |
| 그누보드7 코어 | `>=7.0.0` |
| PHP | `^8.2` |
<!-- @generated:requirements END -->
@@ -151,6 +151,7 @@ php artisan module:activate gnuboard7-hello_module
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -43,7 +43,7 @@ Listeners/LogMemoCreatedListener memo.created 구독 — 부가 작업은
resources/layouts/ admin 2 + user 1
```
이 지도가 곧 **G7 모듈의 규약**입니다 — 검증은 FormRequest, 데이터 접근은 Repository
이 지도가 곧 **그누보드7 모듈의 규약**입니다 — 검증은 FormRequest, 데이터 접근은 Repository
인터페이스, 부가 작업은 훅 리스너, 응답 형태는 Resource. 샘플이 잘못된 본을 보이면 그것을 따라
한 모듈이 전부 같은 형태가 되므로, 이 네 경계는 편의를 위해서도 흐트러뜨리지 않습니다.
@@ -0,0 +1,86 @@
# Hello 모듈 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙(`editor-spec.json`)을 두지 않습니다. 편집기는 코어 기본 팔레트와 활성 템플릿의 스펙만으로 이 확장의 화면을 다룹니다._
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
학습용 샘플 모듈이라 편집기 스펙을 일부러 두지 않았습니다. 이 모듈의 목적은 모듈의
최소 구조(라우트 → 컨트롤러 → 서비스 → 저장소)를 보여 주는 것이고, 편집기 스펙은 그
구조와 무관한 별개 축입니다.
다만 아래 "샘플 데이터와 페이지 상태" 절이 보여 주듯, 이 모듈에는 프리뷰가 비는 자리가
실제로 있습니다. 스펙이 없어도 되는 상태와 스펙이 필요한데 없는 상태는 다릅니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_선언된 편집기 스펙 블록이 없습니다._
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
선언한 블록이 없으므로 표가 비어 있습니다. 이것은 "편집기가 이 모듈을 다루지 않는다"
가 아니라 "이 모듈이 편집기에 아무것도 알려 주지 않는다" 는 뜻입니다 — 편집기는 여전히
이 모듈의 레이아웃을 열 수 있고, 다만 데이터가 붙지 않은 채로 엽니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 팔레트에 항목을 추가하지 않습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일이므로, 이 확장이 팔레트에 얹을 것은 원래 없습니다.
편집기 팔레트는 활성 템플릿의 스펙이 정합니다 — 이 확장에 편집기 스펙이 생기더라도
`componentPalette` 는 여전히 비어 있을 것입니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙을 두지 않아 선언된 샘플 데이터·페이지 상태가 없습니다._
**프리뷰 샘플이 없는 `data_source` 2개** — 편집기 캔버스에서 이 자리만 빈 화면이 됩니다. 실제 화면은 정상이라 오류도 경고도 남지 않습니다.
`memoData` · `memos`
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
`memoData` · `memos` 두 ID 가 미커버입니다. 메모 목록과 폼이 편집기 캔버스에서 빈
채로 보인다는 뜻입니다.
샘플 모듈이므로 이 상태를 그대로 두는 것도 선택입니다 — 다만 그것은 "편집기 스펙이
없으면 어떤 화면이 되는가" 를 보여 주는 교보재로서 의도적으로 남긴 것이지, 문제가
없다는 뜻이 아닙니다. 스펙을 하나 만들어 보는 것이 이 모듈로 할 수 있는 좋은 연습입니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 아직 편집기 스펙을 두지 않습니다. 아래 변경이 생기면 `editor-spec.json` 을 신설합니다._
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
편집기 스펙을 신설해야 하는 시점은 하나입니다 — **이 확장이 소유한 레이아웃에
이 확장만 쓰는 `data_source` 가 생겼을 때**. 그 순간부터 편집기 캔버스의 그 영역은
빈 화면이 되고, 실제 화면은 정상 동작하므로 오류도 경고도 남지 않습니다.
신설 절차는 확장 루트에 `editor-spec.json` 을 만들고 `sampleData.byDataSourceId` 에
그 ID 를 넣는 것으로 시작합니다. 팔레트·컨트롤은 템플릿의 일이므로 넣지 않습니다.
파일을 만든 뒤 update 커맨드로 활성 디렉토리에 반영해야 편집기가 읽습니다 —
`_bundled` 폴백이 없습니다.
<!-- @intent END -->
+2
View File
@@ -136,6 +136,7 @@ CRUD 흐름 자체를 **자기 도메인으로 대체**하는 가장 무거운
- [ ] 비밀글이 관여하는 새 조회 경로(댓글·첨부·검색 결과 등)를 추가할 때 `SecretContentGate` 재적용 (KVE-2026-1914 — 부모에서 한 번 판정하고 끝나지 않는다)
- [ ] `boards`/`board_posts`/`board_comments` 의 count 컬럼(`posts_count`/`comments_count`/`replies_count`/`attachments_count`)은 훅 리스너(`*CountSyncListener`)가 갱신 — Service 에서 직접 증감 금지
- [ ] 게시판 삭제 시 `getDynamicPermissionIdentifiers()`/`getDynamicRoleIdentifiers()`/`getDynamicMenuSlugs()` 가 최신 상태를 반영하도록 동일 트랜잭션에서 정리 (module.php `uninstall()` 의 `chunkById` 패턴 참고 — OFFSET 순회로 삭제 금지)
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 의 동반 의무 표를 따라 `editor-spec.json` 을 함께 갱신 — 샘플이 없는 `data_source` 는 편집기 캔버스에서만 빈 화면이 되고 실제 화면은 정상이라 오류도 경고도 남지 않는다. 반영은 `php artisan module:update sirsoft-board --force`
## 6. 금지 패턴
@@ -186,6 +187,7 @@ npx playwright test modules/_bundled/sirsoft-board/tests/Playwright/specs/<대
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [1.1.0] - 2026-08-24
+4 -3
View File
@@ -1,13 +1,13 @@
# 게시판
**G7 모듈 · sirsoft-board**
**그누보드7 모듈 · sirsoft-board**
게시판 관리를 위한 모듈
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.1.1-0066FF?style=flat-square" alt="version 1.1.1">
<img src="https://img.shields.io/badge/type-%EB%AA%A8%EB%93%88-555555?style=flat-square" alt="type 모듈">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.10-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
</p>
<!-- @generated:badges END -->
@@ -69,7 +69,7 @@ flowchart LR
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.10` |
| 그누보드7 코어 | `>=7.0.10` |
| PHP | `^8.2` |
<!-- @generated:requirements END -->
@@ -147,6 +147,7 @@ _별도의 관리자 설정 항목이 없습니다._
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -0,0 +1,123 @@
# 게시판 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| manifest | `modules/_bundled/sirsoft-board/editor-spec.json` |
| 형태 | 단일 파일 (인라인) |
| 스펙 버전 | `1.0.0` |
| 스타일 시스템 | - |
| 다크 모드 전략 | - |
> 레이아웃 편집기 스펙 — 게시판 모듈 도메인 sampleData/sampleGlobal/states. 실제 admin 레이아웃 data_source ID 전수 스캔 기반 도메인 ID(posts/post/reports/report_detail/reporters_list/boards/boards_list/board_types/form_data/form_meta/settings) + 사용자 게시판 페이지 byEndpointPattern. 공용 인프라(roles/availableChannels/identityProviders/boardIdentity*/boardNotificationDefinitions)는 admin 템플릿 스펙(roles/availableChannels/identityProviders)·코어 프리셋 폴백이 커버.
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
단일 파일로 둔 것은 분량 때문입니다. 게시판 스펙은 도메인 데이터 4블록뿐이라 분할할
이유가 없습니다 — 분할은 템플릿 스펙처럼 한 파일이 만 줄 단위로 커질 때의 장치입니다.
`스타일 시스템`·`다크 모드 전략` 이 비어 있는 것도 의도입니다. 그 둘은 화면을 **그리는**
쪽의 결정이라 템플릿 스펙이 소유합니다. 게시판이 여기에 값을 넣으면 어떤 템플릿을 깔든
게시판이 스타일 체계를 강제하는 셈이 됩니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 블록 | 역할 | 항목 수 | 출처 |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 23 | `editor-spec.json (인라인)` |
| `sampleData.byEndpointPattern` | 엔드포인트 패턴으로 붙는 프리뷰 응답 | 4 | `editor-spec.json (인라인)` |
| `sampleGlobal` | `_global.*` 프리뷰 baseline 시드 | 1 | `editor-spec.json (인라인)` |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 8 | `editor-spec.json (인라인)` |
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
이 네 블록은 "편집기가 게시판 화면을 실제 API 없이 그리려면 무엇이 필요한가" 에서
그대로 나옵니다. `byDataSourceId` 23종은 admin 레이아웃의 `data_source` ID 를 전수
스캔해 맞춘 것이고, `byEndpointPattern` 4종은 사용자 게시판 페이지처럼 ID 가 아니라
호출 주소로 붙는 자리를 덮습니다.
여기에 없는 것이 무엇인지가 더 중요합니다 — `roles`·`availableChannels`·
`identityProviders` 같은 공용 인프라 ID 는 게시판이 쓰지만 게시판이 선언하지 않습니다.
그것들은 admin 템플릿 스펙과 코어 프리셋이 채웁니다. 여기에 같이 넣으면 같은 ID 의
샘플이 두 곳에 생기고, 둘이 갈라져도 아무 오류가 나지 않습니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 `componentPalette` 를 선언하지 않습니다 — 편집기 팔레트에 추가되는 항목이 없습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일입니다. 모듈·플러그인은 레이아웃 JSON 에서 템플릿이
제공하는 컴포넌트를 **쓰기만** 하므로, 편집기 팔레트에 새로 얹을 것이 없습니다. 그래서 이
확장의 스펙은 `componentPalette`·`controls`·`componentCapabilities`·`nesting` 을 비우고
**도메인 데이터**(`sampleData`·`states`)만 담습니다.
팔레트에 무언가를 추가하고 싶다면 그것은 이 확장이 아니라 활성 템플릿
(`sirsoft-admin_basic` / `sirsoft-basic`)의 스펙에 가야 합니다. 여기에 팔레트를 선언하면
템플릿 선언과 같은 자리를 두고 다투게 되고, 어느 쪽이 이기는지가 합본 순서에 좌우됩니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 자리 | 역할 | 개수 | ID |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 23 | `posts` · `post` · `reports` · `report_detail` · `reporters_list` · `boards` · `boards_list` · `board_types` · `form_data` · `form_meta` · `settings` · `availableChannels` … 외 11개 |
| `sampleData.byEndpointPattern` | 엔드포인트 패턴으로 붙는 프리뷰 응답 | 4 | `/api/modules/sirsoft-board/boards/*/posts*` · `/api/modules/sirsoft-board/boards/popular*` · `/api/modules/sirsoft-board/me/*` · `/api/modules/sirsoft-board/users/*/posts*` |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 8 | `/board/:slug/:id` · `/board/:slug` · `/boards` · `/board/:slug/write` · `*/admin/boards/:slug/edit` · `*/admin/board/:slug/:id/edit` · `*/admin/boards/settings` · `*/admin/board/:slug/post/:id` |
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
`states.groups` 8종은 게시판 화면 중 **상태에 따라 다르게 보이는 것**만 골랐습니다.
비밀글 잠금(`/board/:slug/:id`), 목록의 빈 상태(`/board/:slug`), 작성 폼 등입니다. 상태
변종이 없는 화면은 기본 샘플 하나로 충분하므로 등록하지 않습니다.
게시판 레이아웃에 `data_source` 를 새로 붙일 때는 그 ID 가 공용 인프라인지 게시판
도메인인지 먼저 가릅니다. 도메인이면 이 스펙의 `byDataSourceId` 에, 공용이면 템플릿
스펙에 갑니다. 잘못 판단해도 편집기 화면만 비므로 실행 중에는 드러나지 않습니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
편집기 스펙은 JSON 이므로 빌드가 필요 없습니다. 다만 편집기 서빙은 **활성 디렉토리만** 읽으므로(`_bundled` 폴백 없음) 편집 후 반드시 반영합니다:
```bash
php artisan module:update sirsoft-board --force
```
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
위 표는 "무엇을 함께 고치는가" 만 말합니다. 실제로 놓치는 자리는 **반영 절차**입니다 —
편집기가 읽는 것은 활성 디렉토리이고 `_bundled` 폴백이 없으므로, `_bundled` 에서 스펙을
고치고 update 커맨드를 돌리지 않으면 편집기에는 **직전 내용이 그대로 보입니다.** 파일은
고쳤는데 화면이 안 바뀌었다면 거의 이 경우입니다.
또 하나는 검증 시점입니다. 편집기 스펙은 스키마 검증을 통과해도 "레이아웃이 실제로 쓰는
ID 와 맞는가" 는 확인해 주지 않습니다. 그 어긋남은 편집기 캔버스에서만 빈 화면으로
나타나고 실제 화면은 정상이므로, 위 "샘플 데이터와 페이지 상태" 절의 미커버 목록이 유일한
통로입니다.
게시판은 관리자 화면과 사용자 화면을 모두 갖습니다. 관리자 쪽 `data_source` 는
`byDataSourceId` 로 붙지만 사용자 게시판 페이지는 템플릿이 렌더하므로 ID 가 아니라
`byEndpointPattern` 으로 붙습니다 — 사용자 화면을 건드렸는데 관리자 쪽 자리만 고치면
그 화면은 편집기에서 계속 빈 채로 남습니다.
<!-- @intent END -->
@@ -172,6 +172,7 @@ CRUD 를 바꾸고 싶으면 이 4종 중 하나를 잡으면 되고, 이 모듈
- [ ] 목록 응답에 하위 컬렉션(옵션·이미지)을 실을 때 화면이 실제로 그리는 것만 — Repository 의 `relations:` 와 Resource 의 `whenLoaded` 를 함께 본다
- [ ] 결제수단·PG 관련 선언을 바꾸면 기설치본 `order_settings.json` 을 정정하는 업그레이드 스텝 동반 (자기 접두사만, 멱등)
- [ ] 새 관리자 화면을 추가하면 그 화면의 권한(`getPermissions()`)·메뉴(`getAdminMenus()`)·라우트 이름이 서로 가리키는 대상이 일치하는지 확인
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 의 동반 의무 표를 따라 `editor-spec.json` 을 함께 갱신 — 샘플이 없는 `data_source` 는 편집기 캔버스에서만 빈 화면이 되고 실제 화면은 정상이라 오류도 경고도 남지 않는다. 반영은 `php artisan module:update sirsoft-ecommerce --force`
## 6. 금지 패턴
@@ -227,6 +228,7 @@ npx playwright test modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/<
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
### Fixed
+4 -3
View File
@@ -1,13 +1,13 @@
# 이커머스
**G7 모듈 · sirsoft-ecommerce**
**그누보드7 모듈 · sirsoft-ecommerce**
그누보드7 이커머스 모듈 - 상품, 주문, 결제 관리
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.2.1-0066FF?style=flat-square" alt="version 1.2.1">
<img src="https://img.shields.io/badge/type-%EB%AA%A8%EB%93%88-555555?style=flat-square" alt="type 모듈">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.10-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
</p>
<!-- @generated:badges END -->
@@ -96,7 +96,7 @@ flowchart LR
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.10` |
| 그누보드7 코어 | `>=7.0.10` |
| PHP | `^8.2` |
<!-- @generated:requirements END -->
@@ -198,6 +198,7 @@ _별도의 관리자 설정 항목이 없습니다._
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -0,0 +1,125 @@
# 이커머스 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| manifest | `modules/_bundled/sirsoft-ecommerce/editor-spec.json` |
| 형태 | 단일 파일 (인라인) |
| 스펙 버전 | `1.0.0` |
| 스타일 시스템 | - |
| 다크 모드 전략 | - |
> 레이아웃 편집기 스펙 — 이커머스 모듈 도메인 sampleData/sampleGlobal/states. admin 레이아웃 data_source ID 전수 스캔 기반 도메인 ID 28종(상품·주문·브랜드·쿠폰·배송정책·정산·설정 등) byDataSourceId + 사용자 페이지(템플릿 렌더) byEndpointPattern. 공용 인프라(roles/availableChannels/identityProviders/ecommerceIdentity*/ecommerceNotificationDefinitions)는 admin 템플릿 스펙·코어 프리셋 폴백이 커버.
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
이커머스는 저장소에서 가장 큰 모듈이지만 편집기 스펙은 여전히 단일 파일입니다. 스펙
분량을 키우는 것은 팔레트·컨트롤·컴포넌트 역량인데 그 셋은 템플릿이 소유하기 때문입니다.
모듈 쪽에 남는 것은 도메인 데이터라 라우트 239개·레이아웃 206개 규모에도 한 파일에
들어갑니다.
이 사실이 곧 설계 원칙입니다 — 확장이 커진다고 편집기 스펙이 따라 커지지 않습니다.
커진다면 그 확장이 템플릿의 일을 하고 있다는 신호입니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 블록 | 역할 | 항목 수 | 출처 |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 51 | `editor-spec.json (인라인)` |
| `sampleData.byEndpointPattern` | 엔드포인트 패턴으로 붙는 프리뷰 응답 | 7 | `editor-spec.json (인라인)` |
| `sampleGlobal` | `_global.*` 프리뷰 baseline 시드 | 12 | `editor-spec.json (인라인)` |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 17 | `editor-spec.json (인라인)` |
| `actionRecipes` | 친화 명칭 → 액션 JSON 레시피 | 1 | `editor-spec.json (인라인)` |
| `actionChipCandidates` | 동작 데이터 칩 컨텍스트 후보 | 1 | `editor-spec.json (인라인)` |
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
`actionRecipes` 와 `actionChipCandidates` 를 각 1건씩 둔 것이 다른 모듈과 다른
지점입니다. 이커머스에는 운영자가 편집기에서 직접 조립하기 어려운 동작(장바구니·주문
흐름에 얽힌 것)이 있어, 친화 명칭으로 미리 만들어 둔 레시피가 필요합니다.
나머지 네 블록은 게시판과 같은 원리입니다 — admin 레이아웃 `data_source` ID 51종을
전수로 덮고, 사용자 페이지 7종은 호출 주소로 덮습니다. `sampleGlobal` 12종은 통화·로케일
같은 값이 `_global` 에 없으면 상품 카드가 통째로 깨지기 때문에 baseline 으로 박아
둔 것입니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 `componentPalette` 를 선언하지 않습니다 — 편집기 팔레트에 추가되는 항목이 없습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일입니다. 모듈·플러그인은 레이아웃 JSON 에서 템플릿이
제공하는 컴포넌트를 **쓰기만** 하므로, 편집기 팔레트에 새로 얹을 것이 없습니다. 그래서 이
확장의 스펙은 `componentPalette`·`controls`·`componentCapabilities`·`nesting` 을 비우고
**도메인 데이터**(`sampleData`·`states`)만 담습니다.
팔레트에 무언가를 추가하고 싶다면 그것은 이 확장이 아니라 활성 템플릿
(`sirsoft-admin_basic` / `sirsoft-basic`)의 스펙에 가야 합니다. 여기에 팔레트를 선언하면
템플릿 선언과 같은 자리를 두고 다투게 되고, 어느 쪽이 이기는지가 합본 순서에 좌우됩니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 자리 | 역할 | 개수 | ID |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 51 | `transactions` · `products` · `product` · `categories` · `orders` · `order` · `reviews` · `brands` · `carriers` · `active_carriers` · `coupons` · `coupon` … 외 39개 |
| `sampleData.byEndpointPattern` | 엔드포인트 패턴으로 붙는 프리뷰 응답 | 7 | `/api/modules/sirsoft-ecommerce/products*` · `/api/modules/sirsoft-ecommerce/cart*` · `/api/modules/sirsoft-ecommerce/checkout*` · `/api/modules/sirsoft-ecommerce/wishlist*` · `/api/modules/sirsoft-ecommerce/user/orders*` · `/api/modules/sirsoft-ecommerce/user/addresses*` · `/api/modules/sirsoft-ecommerce/user/inquiries*` |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 17 | `/*?/products/:product_code` · `/*?/products` · `/*?/cart` · `/*?/checkout` · `/*?/orders/:id/complete` · `/*?/reorder/:id` · `*/admin/ecommerce/products/:itemCode/edit` · `*/admin/ecommerce/promotion-coupons/:id/edit` · `*/admin/ecommerce/shipping-policies/:id/edit` · `*/admin/ecommerce/settings` · `*/admin/ecommerce/mileage-deposit-settings` · `/*?/guest/orders` … 외 5개 |
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
`states.groups` 17종은 이커머스에서 상태가 실제로 화면을 가르는 자리입니다 — 품절
상품, 빈 장바구니, 비회원 주문 조회, 재주문 등입니다. 상태를 늘리는 기준은 "그 상태에서
운영자가 화면을 따로 손봐야 하는가" 입니다. 값만 다르고 구조가 같은 경우는 변종을
만들지 않습니다.
`sampleGlobal` 에 통화 관련 값을 둘 때는 특정 통화를 정답으로 박지 않도록 주의합니다.
기본 통화는 설정이 정하므로, 샘플이 특정 통화를 전제하면 편집기 프리뷰만 그 통화로
고정되어 다른 통화 상점의 운영자에게 잘못된 화면을 보여 줍니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
편집기 스펙은 JSON 이므로 빌드가 필요 없습니다. 다만 편집기 서빙은 **활성 디렉토리만** 읽으므로(`_bundled` 폴백 없음) 편집 후 반드시 반영합니다:
```bash
php artisan module:update sirsoft-ecommerce --force
```
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
위 표는 "무엇을 함께 고치는가" 만 말합니다. 실제로 놓치는 자리는 **반영 절차**입니다 —
편집기가 읽는 것은 활성 디렉토리이고 `_bundled` 폴백이 없으므로, `_bundled` 에서 스펙을
고치고 update 커맨드를 돌리지 않으면 편집기에는 **직전 내용이 그대로 보입니다.** 파일은
고쳤는데 화면이 안 바뀌었다면 거의 이 경우입니다.
또 하나는 검증 시점입니다. 편집기 스펙은 스키마 검증을 통과해도 "레이아웃이 실제로 쓰는
ID 와 맞는가" 는 확인해 주지 않습니다. 그 어긋남은 편집기 캔버스에서만 빈 화면으로
나타나고 실제 화면은 정상이므로, 위 "샘플 데이터와 페이지 상태" 절의 미커버 목록이 유일한
통로입니다.
이커머스는 `sampleGlobal` 이 12종으로 가장 많습니다. 레이아웃이 `_global.*` 을 새로
읽기 시작했는데 baseline 을 안 넣으면 그 값이 `undefined` 가 되어, 표현식이 통째로
falsy 로 떨어지며 **영역 전체가 사라집니다.** 값 하나가 비는 것보다 알아채기 어렵습니다.
<!-- @intent END -->
@@ -23,7 +23,7 @@ modules/_bundled/sirsoft-ecommerce/
## 데이터 생성 위치 분리 (CRITICAL)
E2E spec 이 "특정 유저 + 특정 역할 + 특정 도메인 상황" 에서 동작하려면 백엔드 데이터 생성 코드가
필요하다. **그 코드는 데이터를 소유한 영역에 위치해야 한다** — 기존 G7 의 Seeder/Factory 분리 원칙과 동일.
필요하다. **그 코드는 데이터를 소유한 영역에 위치해야 한다** — 기존 그누보드7 의 Seeder/Factory 분리 원칙과 동일.
| 데이터 종류 | 위치 |
|---|---|
+2
View File
@@ -149,6 +149,7 @@
- [ ] 코어 검색·SEO·ckeditor5 의 훅 이름이 바뀌면 이 모듈의 구독 5종이 조용히 끊기므로 함께 확인
- [ ] 첨부 제한(`attachment.*`)은 `config/settings/defaults.json` 이 SSoT — 서비스에서 리터럴로 재클램프하지 않는다
- [ ] 활동 로그 항목을 추가하면 코어 `lang/{ko,en}/activity_log.php` 의 action 라벨·description 과 번들 ja 팩까지 동반
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 의 동반 의무 표를 따라 `editor-spec.json` 을 함께 갱신 — 샘플이 없는 `data_source` 는 편집기 캔버스에서만 빈 화면이 되고 실제 화면은 정상이라 오류도 경고도 남지 않는다. 반영은 `php artisan module:update sirsoft-page --force`
## 6. 금지 패턴
@@ -202,6 +203,7 @@ npx playwright test modules/_bundled/sirsoft-page/tests/Playwright/specs/<대상
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [1.1.0] - 2026-08-24
+4 -3
View File
@@ -1,13 +1,13 @@
# 페이지
**G7 모듈 · sirsoft-page**
**그누보드7 모듈 · sirsoft-page**
정적 페이지(정보/정책/안내) 관리 모듈
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.1.1-0066FF?style=flat-square" alt="version 1.1.1">
<img src="https://img.shields.io/badge/type-%EB%AA%A8%EB%93%88-555555?style=flat-square" alt="type 모듈">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.10-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
</p>
<!-- @generated:badges END -->
@@ -73,7 +73,7 @@ flowchart LR
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.10` |
| 그누보드7 코어 | `>=7.0.10` |
| PHP | `^8.2` |
<!-- @generated:requirements END -->
@@ -156,6 +156,7 @@ _별도의 관리자 설정 항목이 없습니다._
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -0,0 +1,115 @@
# 페이지 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| manifest | `modules/_bundled/sirsoft-page/editor-spec.json` |
| 형태 | 단일 파일 (인라인) |
| 스펙 버전 | `1.0.0` |
| 스타일 시스템 | - |
| 다크 모드 전략 | - |
> 레이아웃 편집기 스펙 — 페이지 모듈 도메인 sampleData/states. 실제 admin 레이아웃 data_source ID 4종 전수(pages/page/pageData/versions) + 사용자 페이지(/p/:slug, 약관/개인정보) byEndpointPattern. sampleGlobal 은 페이지 도메인이 _global keyspace 를 두지 않아 미작성(&#039;필요 시&#039; 조건부 — 정당).
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
페이지 모듈의 스펙은 세 블록뿐입니다. 화면이 "목록 · 편집 · 공개 보기" 로 단순하고,
운영자가 편집기에서 손대는 대상이 페이지 **내용**이 아니라 그것을 감싸는 레이아웃이기
때문입니다.
`sampleGlobal` 을 두지 않은 것은 누락이 아닙니다 — 페이지 도메인은 `_global` 키를
자기 것으로 쓰지 않습니다. 필요 없는 블록을 빈 값으로라도 선언해 두면 다음 사람이 그
빈 값을 채워야 할 자리로 오해합니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 블록 | 역할 | 항목 수 | 출처 |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 6 | `editor-spec.json (인라인)` |
| `sampleData.byEndpointPattern` | 엔드포인트 패턴으로 붙는 프리뷰 응답 | 3 | `editor-spec.json (인라인)` |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 3 | `editor-spec.json (인라인)` |
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
`byDataSourceId` 6종 중 `termsContent`·`privacyContent` 는 다른 넷과 성격이 다릅니다.
약관·개인정보 페이지는 슬러그가 고정된 특수 페이지라 편집기에서 그 자리에 무엇이 들어갈지
미리 보여 줘야 합니다. `byEndpointPattern` 3종도 같은 이유로 이 둘을 따로 덮습니다.
`states.groups` 3종은 공개 페이지와 관리자 편집·상세를 하나씩 맡습니다. 페이지는 상태
변종이 적은 도메인이라 이 수가 늘어난다면 화면이 복잡해지고 있다는 신호입니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 `componentPalette` 를 선언하지 않습니다 — 편집기 팔레트에 추가되는 항목이 없습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일입니다. 모듈·플러그인은 레이아웃 JSON 에서 템플릿이
제공하는 컴포넌트를 **쓰기만** 하므로, 편집기 팔레트에 새로 얹을 것이 없습니다. 그래서 이
확장의 스펙은 `componentPalette`·`controls`·`componentCapabilities`·`nesting` 을 비우고
**도메인 데이터**(`sampleData`·`states`)만 담습니다.
팔레트에 무언가를 추가하고 싶다면 그것은 이 확장이 아니라 활성 템플릿
(`sirsoft-admin_basic` / `sirsoft-basic`)의 스펙에 가야 합니다. 여기에 팔레트를 선언하면
템플릿 선언과 같은 자리를 두고 다투게 되고, 어느 쪽이 이기는지가 합본 순서에 좌우됩니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 자리 | 역할 | 개수 | ID |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 6 | `pages` · `page` · `pageData` · `versions` · `termsContent` · `privacyContent` |
| `sampleData.byEndpointPattern` | 엔드포인트 패턴으로 붙는 프리뷰 응답 | 3 | `/api/modules/sirsoft-page/pages/terms` · `/api/modules/sirsoft-page/pages/privacy` · `/api/modules/sirsoft-page/pages/*` |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 3 | `/page/:slug` · `*/admin/pages/:id/edit` · `*/admin/pages/:id` |
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
페이지 모듈에서 주의할 것은 `/p/:slug` 처럼 **슬러그가 열려 있는 라우트**입니다.
편집기는 특정 슬러그 하나를 골라 프리뷰를 그리므로, 그 샘플이 실제 운영 페이지 중
가장 단순한 것을 닮아 있으면 복잡한 페이지에서 레이아웃이 깨지는 것을 편집기에서
미리 볼 수 없습니다.
샘플을 고를 때는 가장 짧은 페이지가 아니라 **가장 많은 요소를 가진 페이지**를 기준으로
삼습니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
편집기 스펙은 JSON 이므로 빌드가 필요 없습니다. 다만 편집기 서빙은 **활성 디렉토리만** 읽으므로(`_bundled` 폴백 없음) 편집 후 반드시 반영합니다:
```bash
php artisan module:update sirsoft-page --force
```
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
위 표는 "무엇을 함께 고치는가" 만 말합니다. 실제로 놓치는 자리는 **반영 절차**입니다 —
편집기가 읽는 것은 활성 디렉토리이고 `_bundled` 폴백이 없으므로, `_bundled` 에서 스펙을
고치고 update 커맨드를 돌리지 않으면 편집기에는 **직전 내용이 그대로 보입니다.** 파일은
고쳤는데 화면이 안 바뀌었다면 거의 이 경우입니다.
또 하나는 검증 시점입니다. 편집기 스펙은 스키마 검증을 통과해도 "레이아웃이 실제로 쓰는
ID 와 맞는가" 는 확인해 주지 않습니다. 그 어긋남은 편집기 캔버스에서만 빈 화면으로
나타나고 실제 화면은 정상이므로, 위 "샘플 데이터와 페이지 상태" 절의 미커버 목록이 유일한
통로입니다.
<!-- @intent END -->
@@ -125,6 +125,7 @@ Filter 구독에는 `'type' => 'filter'` 선언이 반드시 필요합니다.
- [ ] 구독 대상 모듈의 훅 이름이 바뀌면 이 플러그인이 조용히 아무 일도 하지 않게 된다
- [ ] `docs/extension/sample-extensions.md` 의 계층 표와 어긋나지 않는지 확인 (파일을 추가·삭제했다면 그 표도 갱신)
- [ ] 플러그인은 완전한 페이지 레이아웃을 등록할 수 없다 — 설정 화면과 `layout_extensions` 만
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 를 확인 — 이 확장은 편집기 스펙이 없어도 되는 상태(공용 ID 만 사용)다. 이 확장만 쓰는 `data_source` 를 새로 붙이는 순간 `editor-spec.json` 신설이 필요해진다
## 6. 금지 패턴
@@ -173,5 +174,6 @@ php vendor/bin/phpunit plugins/_bundled/gnuboard7-hello_plugin/tests --filter='<
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [0.1.1] - 2026-08-17
@@ -1,13 +1,13 @@
# Hello 플러그인
**G7 플러그인 · gnuboard7-hello_plugin**
**그누보드7 플러그인 · gnuboard7-hello_plugin**
학습용 최소 샘플 플러그인 (Hello 모듈 훅 소비)
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-0.1.2-0066FF?style=flat-square" alt="version 0.1.2">
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.0-1F883D?style=flat-square" alt="G7 &gt;=7.0.0">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.0-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.0">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
<img src="https://img.shields.io/badge/requires-gnuboard7--hello__module-BF8700?style=flat-square" alt="requires gnuboard7-hello_module">
</p>
@@ -72,7 +72,7 @@ flowchart LR
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.0` |
| 그누보드7 코어 | `>=7.0.0` |
| PHP | `^8.2` |
| 의존 모듈 | `gnuboard7-hello_module` `>=0.1.0` |
<!-- @generated:requirements END -->
@@ -158,6 +158,7 @@ php artisan plugin:activate gnuboard7-hello_plugin
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
<!-- @generated:doc-toc END -->
@@ -0,0 +1,82 @@
# Hello 플러그인 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙(`editor-spec.json`)을 두지 않습니다. 편집기는 코어 기본 팔레트와 활성 템플릿의 스펙만으로 이 확장의 화면을 다룹니다._
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
학습용 샘플 플러그인이라 편집기 스펙을 두지 않았고, 지금 상태에서는 **둘 필요도
없습니다.** 이 플러그인이 소유한 화면은 설정 화면 하나이고 그 화면이 읽는 `settings` 는
여러 확장이 공유하는 공용 ID 라 admin 템플릿 스펙이 이미 채웁니다.
이것이 "스펙 없음" 의 정상 형태입니다 — 아래 미커버 목록이 비어 있다는 사실이 그
근거입니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_선언된 편집기 스펙 블록이 없습니다._
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
선언한 블록이 없습니다. 공용 ID 만 쓰는 확장은 자기 스펙을 갖지 않는 것이 규율에
맞습니다 — 같은 ID 의 샘플을 확장마다 두면 어느 것이 쓰이는지가 합본 순서에 좌우되고,
둘이 갈라져도 오류가 나지 않습니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 팔레트에 항목을 추가하지 않습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일이므로, 이 확장이 팔레트에 얹을 것은 원래 없습니다.
편집기 팔레트는 활성 템플릿의 스펙이 정합니다 — 이 확장에 편집기 스펙이 생기더라도
`componentPalette` 는 여전히 비어 있을 것입니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙을 두지 않아 선언된 샘플 데이터·페이지 상태가 없습니다._
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
미커버가 없습니다. 이 플러그인의 레이아웃이 쓰는 `data_source` 는 전부 번들 템플릿
스펙이 채우므로, 편집기에서 설정 화면을 열면 값이 채워진 상태로 보입니다.
스펙을 갖지 않은 확장이 이 상태여야 정상입니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 아직 편집기 스펙을 두지 않습니다. 아래 변경이 생기면 `editor-spec.json` 을 신설합니다._
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
편집기 스펙을 신설해야 하는 시점은 하나입니다 — **이 확장이 소유한 레이아웃에
이 확장만 쓰는 `data_source` 가 생겼을 때**. 그 순간부터 편집기 캔버스의 그 영역은
빈 화면이 되고, 실제 화면은 정상 동작하므로 오류도 경고도 남지 않습니다.
신설 절차는 확장 루트에 `editor-spec.json` 을 만들고 `sampleData.byDataSourceId` 에
그 ID 를 넣는 것으로 시작합니다. 팔레트·컨트롤은 템플릿의 일이므로 넣지 않습니다.
파일을 만든 뒤 update 커맨드로 활성 디렉토리에 반영해야 편집기가 읽습니다 —
`_bundled` 폴백이 없습니다.
<!-- @intent END -->
@@ -148,6 +148,7 @@
- [ ] 정리 커맨드의 판정 로직을 고쳤다면 fail-open 가드(`hasPotentiallyMissingSources()`)가 여전히 앞에 있는지 확인 — 이 가드가 빠지면 이미지가 조용히 지워진다
- [ ] 편집기 폴백 경로를 고쳤다면 저장 계약(`{name}_mode`)과 재시도 시 내용 승계가 유지되는지 확인
- [ ] 프론트엔드를 고쳤다면 Playwright 위지윅 spec 을 함께 갱신·실행한다 (단위 테스트만으로는 편집기 장착 회귀가 드러나지 않는다)
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 를 확인 — 이 확장은 편집기 스펙이 없어 `ckeditor5Uploads` 가 편집기 캔버스에서 빈 화면으로 보인다. `data_source` 를 더 늘리면 그 자리도 같은 상태가 된다
## 6. 금지 패턴
@@ -202,6 +203,7 @@ npx playwright test plugins/_bundled/sirsoft-ckeditor5/tests/Playwright/specs/<
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -11,6 +11,8 @@
- CKEditor 5 본체·스타일·번역 파일을 플러그인에 함께 담았습니다. 이제 외부 CDN 에 연결하지 않고 사이트 자신의 서버에서 불러오므로, 폐쇄망이나 외부 접속이 제한된 환경에서도 에디터가 동작합니다.
- 에디터를 불러오지 못한 경우 안내와 함께 임시 입력창으로 자동 전환됩니다. 작성한 내용은 그대로 저장되고, 이미 저장된 글을 수정할 때는 기존 본문이 임시 입력창에 그대로 실립니다. [다시 시도] 로 편집기를 되살리면 입력해 둔 내용이 그대로 이어집니다.
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
### Fixed
+4 -3
View File
@@ -1,13 +1,13 @@
# CKEditor 5 WYSIWYG 에디터
**G7 플러그인 · sirsoft-ckeditor5**
**그누보드7 플러그인 · sirsoft-ckeditor5**
CKEditor 5를 이용한 WYSIWYG 에디터 플러그인입니다. 플러그인 설치만으로 기존 HtmlEditor가 교체됩니다.
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.0.3-0066FF?style=flat-square" alt="version 1.0.3">
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.10-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
</p>
<!-- @generated:badges END -->
@@ -84,7 +84,7 @@ flowchart LR
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.10` |
| 그누보드7 코어 | `>=7.0.10` |
| PHP | `^8.2` |
<!-- @generated:requirements END -->
@@ -178,6 +178,7 @@ php artisan plugin:update sirsoft-ckeditor5 --force
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -0,0 +1,85 @@
# CKEditor 5 WYSIWYG 에디터 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙(`editor-spec.json`)을 두지 않습니다. 편집기는 코어 기본 팔레트와 활성 템플릿의 스펙만으로 이 확장의 화면을 다룹니다._
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
위지윅 에디터 플러그인이지만 편집기 스펙은 두지 않았습니다. 이 플러그인이 다루는 것은
**본문 작성기**이고, 레이아웃 편집기가 다루는 것은 그 작성기를 **배치하는 화면**이라
서로 다른 층이기 때문입니다.
에디터 자체의 팔레트(툴바 구성)는 이 플러그인의 설정 화면에서 정하지, 레이아웃 편집기
스펙과는 무관합니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_선언된 편집기 스펙 블록이 없습니다._
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
선언한 블록이 없습니다. 다만 이 플러그인은 설정 화면 외에 **업로드 관리 화면**을 하나
더 갖고 있고, 그 화면은 자기 도메인 데이터를 읽습니다 — 아래 미커버 목록에 그 결과가
드러납니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 팔레트에 항목을 추가하지 않습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일이므로, 이 확장이 팔레트에 얹을 것은 원래 없습니다.
편집기 팔레트는 활성 템플릿의 스펙이 정합니다 — 이 확장에 편집기 스펙이 생기더라도
`componentPalette` 는 여전히 비어 있을 것입니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙을 두지 않아 선언된 샘플 데이터·페이지 상태가 없습니다._
**프리뷰 샘플이 없는 `data_source` 1개** — 편집기 캔버스에서 이 자리만 빈 화면이 됩니다. 실제 화면은 정상이라 오류도 경고도 남지 않습니다.
`ckeditor5Uploads`
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
`ckeditor5Uploads` 가 미커버입니다. 업로드 관리 화면의 목록 영역이 편집기 캔버스에서
빈 채로 보입니다.
설정 화면 쪽 `settings` 는 공용 ID 라 템플릿 스펙이 채우므로 문제가 없습니다. 즉 이
플러그인은 **화면 둘 중 하나만** 편집기에서 온전히 보이는 상태입니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 아직 편집기 스펙을 두지 않습니다. 아래 변경이 생기면 `editor-spec.json` 을 신설합니다._
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
편집기 스펙을 신설해야 하는 시점은 하나입니다 — **이 확장이 소유한 레이아웃에
이 확장만 쓰는 `data_source` 가 생겼을 때**. 그 순간부터 편집기 캔버스의 그 영역은
빈 화면이 되고, 실제 화면은 정상 동작하므로 오류도 경고도 남지 않습니다.
신설 절차는 확장 루트에 `editor-spec.json` 을 만들고 `sampleData.byDataSourceId` 에
그 ID 를 넣는 것으로 시작합니다. 팔레트·컨트롤은 템플릿의 일이므로 넣지 않습니다.
파일을 만든 뒤 update 커맨드로 활성 디렉토리에 반영해야 편집기가 읽습니다 —
`_bundled` 폴백이 없습니다.
<!-- @intent END -->
@@ -126,6 +126,7 @@ Daum SDK 를 로드 → 컨테이너 `onMount` 에서 `sirsoft-daum_postcode.set
- [ ] `dist/` 는 커밋되는 배포 산출물 — TS 를 고쳤으면 `--production` 재빌드 후 커밋 (`sourceMappingURL` 잔존 금지)
- [ ] 조각이 붙는 확장점(`address_search_slot`)을 여는 화면이 그 자리를 없애면 오류 없이 사라진다 — 대상 확장 업그레이드 후 노출 확인
- [ ] 프론트엔드를 고쳤다면 Playwright spec 을 함께 갱신·실행한다 (단위 테스트만으로는 장착 회귀가 드러나지 않는다)
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 를 확인 — 이 확장은 편집기 스펙이 없어도 되는 상태(공용 ID 만 사용)다. 이 확장만 쓰는 `data_source` 를 새로 붙이는 순간 `editor-spec.json` 신설이 필요해진다
## 6. 금지 패턴
@@ -171,5 +172,6 @@ cd plugins/_bundled/sirsoft-daum_postcode && powershell -Command "npm run test:r
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
### Fixed
@@ -1,13 +1,13 @@
# Daum 우편번호
**G7 플러그인 · sirsoft-daum_postcode**
**그누보드7 플러그인 · sirsoft-daum_postcode**
Daum 우편번호 서비스를 통한 주소 검색 기능을 제공하는 플러그인입니다. API 키 없이 무료로 사용할 수 있습니다.
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.0.3-0066FF?style=flat-square" alt="version 1.0.3">
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.10-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
</p>
<!-- @generated:badges END -->
@@ -72,7 +72,7 @@ flowchart LR
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.10` |
| 그누보드7 코어 | `>=7.0.10` |
| PHP | `^8.2` |
| 외부 스크립트 호스트 | `t1.daumcdn.net` |
<!-- @generated:requirements END -->
@@ -158,6 +158,7 @@ php artisan plugin:update sirsoft-daum_postcode --force
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
<!-- @generated:doc-toc END -->
@@ -0,0 +1,79 @@
# Daum 우편번호 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙(`editor-spec.json`)을 두지 않습니다. 편집기는 코어 기본 팔레트와 활성 템플릿의 스펙만으로 이 확장의 화면을 다룹니다._
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
주소 검색 플러그인은 자기 화면을 거의 갖지 않습니다. 주소 검색 창은 외부 서비스가
띄우는 것이고, 이 플러그인이 소유한 화면은 그 창의 모양·크기를 정하는 설정 하나입니다.
그래서 편집기 스펙을 두지 않으며, 지금 상태에서는 둘 필요도 없습니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_선언된 편집기 스펙 블록이 없습니다._
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
선언한 블록이 없습니다. 설정 화면이 읽는 `settings` 는 공용 ID 라 admin 템플릿 스펙이
채웁니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 팔레트에 항목을 추가하지 않습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일이므로, 이 확장이 팔레트에 얹을 것은 원래 없습니다.
편집기 팔레트는 활성 템플릿의 스펙이 정합니다 — 이 확장에 편집기 스펙이 생기더라도
`componentPalette` 는 여전히 비어 있을 것입니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙을 두지 않아 선언된 샘플 데이터·페이지 상태가 없습니다._
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
미커버가 없습니다. 이 플러그인의 레이아웃이 쓰는 `data_source` 는 전부 번들 템플릿
스펙이 채우므로 편집기에서 설정 화면이 온전히 보입니다.
주소 검색 창 자체는 외부 스크립트가 띄우므로 편집기 캔버스에는 나타나지 않습니다.
그것은 스펙으로 해결할 수 있는 종류가 아닙니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 아직 편집기 스펙을 두지 않습니다. 아래 변경이 생기면 `editor-spec.json` 을 신설합니다._
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
편집기 스펙을 신설해야 하는 시점은 하나입니다 — **이 확장이 소유한 레이아웃에
이 확장만 쓰는 `data_source` 가 생겼을 때**. 그 순간부터 편집기 캔버스의 그 영역은
빈 화면이 되고, 실제 화면은 정상 동작하므로 오류도 경고도 남지 않습니다.
신설 절차는 확장 루트에 `editor-spec.json` 을 만들고 `sampleData.byDataSourceId` 에
그 ID 를 넣는 것으로 시작합니다. 팔레트·컨트롤은 템플릿의 일이므로 넣지 않습니다.
파일을 만든 뒤 update 커맨드로 활성 디렉토리에 반영해야 편집기가 읽습니다 —
`_bundled` 폴백이 없습니다.
<!-- @intent END -->
+2
View File
@@ -129,6 +129,7 @@ necessary 4종(`XSRF-TOKEN`/세션/`laravel_maintenance`/`gdpr_session`)을 제
- [ ] `gdpr_user_consent_histories` 는 append-only — UPDATE/DELETE 로 기존 행을 고치지 않는다 (완전삭제 시 익명화 UPDATE 예외는 `GdprUserDeleteListener` 단일 지점에서만 수행)
- [ ] 새 자동 차단 카테고리(기능/분석/마케팅 외)를 추가하면 배너 UI·`blocked_domains` 스키마·차단 스크립트 3곳 동기화
- [ ] `CookieConsentMiddleware` 의 strictly-necessary allowlist(4종)를 확장할 때는 ePrivacy Art.5(3) 면제 항목인지 먼저 검토 — 임의로 늘리면 동의 전 차단 원칙이 무력화된다
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 의 동반 의무 표를 따라 `editor-spec.json` 을 함께 갱신 — 샘플이 없는 `data_source` 는 편집기 캔버스에서만 빈 화면이 되고 실제 화면은 정상이라 오류도 경고도 남지 않는다. 반영은 `php artisan plugin:update sirsoft-gdpr --force`
## 6. 금지 패턴
@@ -179,6 +180,7 @@ npx playwright test plugins/_bundled/sirsoft-gdpr/tests/Playwright/specs/<대상
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
### Fixed
+4 -3
View File
@@ -1,13 +1,13 @@
# GDPR
**G7 플러그인 · sirsoft-gdpr**
**그누보드7 플러그인 · sirsoft-gdpr**
GDPR·개인정보보호법 대응 쿠키 동의 배너와 동의 이력 관리를 제공하는 플러그인
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.0.4-0066FF?style=flat-square" alt="version 1.0.4">
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.6-1F883D?style=flat-square" alt="G7 &gt;=7.0.6">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.6-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.6">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
</p>
<!-- @generated:badges END -->
@@ -69,7 +69,7 @@ flowchart LR
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.6` |
| 그누보드7 코어 | `>=7.0.6` |
| PHP | `^8.2` |
<!-- @generated:requirements END -->
@@ -185,6 +185,7 @@ Google Analytics, Facebook Pixel, Kakao Pixel 등)가 시드되어 있고 운영
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -0,0 +1,112 @@
# GDPR (일반 데이터 보호 규정) — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| manifest | `plugins/_bundled/sirsoft-gdpr/editor-spec.json` |
| 형태 | 단일 파일 (인라인) |
| 스펙 버전 | `1.0.0` |
| 스타일 시스템 | - |
| 다크 모드 전략 | - |
> GDPR 플러그인 레이아웃 편집기 샘플 데이터 (쿠키 동의 설정 / 동의 이력 / 개인정보 정책 버전).
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
GDPR 플러그인은 관리자 설정·동의 이력 화면과 **사용자 화면에 얹히는 쿠키 배너**를
함께 갖습니다. 스펙이 두 블록만으로 끝나는 것은 이 플러그인이 컴포넌트를 만들지 않고
템플릿 컴포넌트로 배너를 조립하기 때문입니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 블록 | 역할 | 항목 수 | 출처 |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 9 | `editor-spec.json (인라인)` |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 2 | `editor-spec.json (인라인)` |
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
`byDataSourceId` 9종이 설정·정책 버전·동의 이력 세 갈래를 덮습니다. `gdprPublicSettings`
와 `gdprSettings` 가 따로 있는 것이 이 스펙의 핵심입니다 — 공개 응답과 관리자 응답은
같은 저장값에서 나오지만 **내보내는 항목이 다릅니다.** 샘플을 하나로 합치면 편집기에서
사용자 배너를 편집할 때 관리자만 볼 수 있는 항목까지 보이게 됩니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 `componentPalette` 를 선언하지 않습니다 — 편집기 팔레트에 추가되는 항목이 없습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일입니다. 모듈·플러그인은 레이아웃 JSON 에서 템플릿이
제공하는 컴포넌트를 **쓰기만** 하므로, 편집기 팔레트에 새로 얹을 것이 없습니다. 그래서 이
확장의 스펙은 `componentPalette`·`controls`·`componentCapabilities`·`nesting` 을 비우고
**도메인 데이터**(`sampleData`·`states`)만 담습니다.
팔레트에 무언가를 추가하고 싶다면 그것은 이 확장이 아니라 활성 템플릿
(`sirsoft-admin_basic` / `sirsoft-basic`)의 스펙에 가야 합니다. 여기에 팔레트를 선언하면
템플릿 선언과 같은 자리를 두고 다투게 되고, 어느 쪽이 이기는지가 합본 순서에 좌우됩니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 자리 | 역할 | 개수 | ID |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 9 | `gdprSettings` · `gdprPolicyVersionCurrent` · `gdprPolicyVersionHistory` · `gdprPolicyVersionSnapshot` · `gdprConsentLog` · `gdprPublicSettings` · `gdprMyConsent` · `gdprMeConsents` · `gdprMyConsents` |
| `sampleData.byEndpointPattern` | 엔드포인트 패턴으로 붙는 프리뷰 응답 | 미선언 | - |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 2 | `*/admin/plugins/sirsoft-gdpr/consent-log` · `_user_base` |
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
`states.groups` 가 `_user_base` 를 범위로 갖는 것이 이 플러그인의 특징입니다. 쿠키
배너는 특정 라우트가 아니라 **모든 사용자 화면의 베이스 레이아웃**에 얹히므로, 편집기가
배너를 보여 주려면 베이스 레이아웃에 상태를 주입해야 합니다.
배너는 동의 전에만 보입니다. 편집기 캔버스는 정적 시뮬레이션이라 "아직 동의하지 않은
방문자" 상태를 만들어 주지 않으면 배너가 화면에 나타나지 않아 **편집 자체가 불가능**합니다.
`_user_base` 상태 변종이 그 역할입니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
편집기 스펙은 JSON 이므로 빌드가 필요 없습니다. 다만 편집기 서빙은 **활성 디렉토리만** 읽으므로(`_bundled` 폴백 없음) 편집 후 반드시 반영합니다:
```bash
php artisan plugin:update sirsoft-gdpr --force
```
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
위 표는 "무엇을 함께 고치는가" 만 말합니다. 실제로 놓치는 자리는 **반영 절차**입니다 —
편집기가 읽는 것은 활성 디렉토리이고 `_bundled` 폴백이 없으므로, `_bundled` 에서 스펙을
고치고 update 커맨드를 돌리지 않으면 편집기에는 **직전 내용이 그대로 보입니다.** 파일은
고쳤는데 화면이 안 바뀌었다면 거의 이 경우입니다.
또 하나는 검증 시점입니다. 편집기 스펙은 스키마 검증을 통과해도 "레이아웃이 실제로 쓰는
ID 와 맞는가" 는 확인해 주지 않습니다. 그 어긋남은 편집기 캔버스에서만 빈 화면으로
나타나고 실제 화면은 정상이므로, 위 "샘플 데이터와 페이지 상태" 절의 미커버 목록이 유일한
통로입니다.
동의 항목을 추가·제거할 때는 `gdprPublicSettings` 와 `gdprSettings` 샘플을 **함께**
고칩니다. 한쪽만 고치면 편집기에서 관리자 화면과 사용자 배너가 서로 다른 항목 목록을
보여 주는데, 어느 쪽이 맞는지 화면만 봐서는 알 수 없습니다.
<!-- @intent END -->
@@ -143,6 +143,7 @@
- [ ] 레이아웃 조각 5개는 대상 화면의 슬롯이 사라지면 오류 없이 빠진다 — 템플릿·코어 회원 화면 업그레이드 후 노출 확인
- [ ] 약관 페이지 slug 설정은 `sirsoft-page` 모듈의 페이지를 가리킨다 (manifest 의존 `>=1.0.0`)
- [ ] 동의 항목을 늘릴 때는 설정만 바꾼다 — 마이그레이션이 필요해졌다면 EAV 구조를 벗어난 설계라는 신호
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 의 동반 의무 표를 따라 `editor-spec.json` 을 함께 갱신 — 샘플이 없는 `data_source` 는 편집기 캔버스에서만 빈 화면이 되고 실제 화면은 정상이라 오류도 경고도 남지 않는다. 반영은 `php artisan plugin:update sirsoft-marketing --force`
## 6. 금지 패턴
@@ -193,6 +194,7 @@ cd plugins/_bundled/sirsoft-marketing && powershell -Command "npm run test:run -
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [1.0.3] - 2026-08-22
+4 -3
View File
@@ -1,13 +1,13 @@
# 마케팅 동의
**G7 플러그인 · sirsoft-marketing**
**그누보드7 플러그인 · sirsoft-marketing**
이메일 구독, 마케팅 동의, 제3자 제공 동의 등을 관리하는 플러그인
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.0.4-0066FF?style=flat-square" alt="version 1.0.4">
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.0-1F883D?style=flat-square" alt="G7 &gt;=7.0.0">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.0-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.0">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
<img src="https://img.shields.io/badge/requires-sirsoft--page-BF8700?style=flat-square" alt="requires sirsoft-page">
</p>
@@ -78,7 +78,7 @@ flowchart LR
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.0` |
| 그누보드7 코어 | `>=7.0.0` |
| PHP | `^8.2` |
| 의존 모듈 | `sirsoft-page` `>=1.0.0` |
<!-- @generated:requirements END -->
@@ -174,6 +174,7 @@ php artisan plugin:update sirsoft-marketing --force
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -0,0 +1,111 @@
# 마케팅 동의 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| manifest | `plugins/_bundled/sirsoft-marketing/editor-spec.json` |
| 형태 | 단일 파일 (인라인) |
| 스펙 버전 | `1.0.0` |
| 스타일 시스템 | - |
| 다크 모드 전략 | - |
> 마케팅 동의 플러그인 레이아웃 편집기 샘플 데이터.
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
마케팅 동의 플러그인의 스펙은 `sampleData` 한 블록, ID 하나가 전부입니다. 이 플러그인이
소유한 화면이 설정 화면 하나뿐이고 그 화면이 읽는 도메인 데이터가 `marketing_settings`
하나이기 때문입니다.
스펙이 작다는 것이 곧 부실을 뜻하지는 않습니다. 필요한 만큼만 선언하는 것이 규율이고,
쓰지 않는 블록을 빈 값으로 채워 두면 다음 사람이 그것을 채워야 할 자리로 오해합니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 블록 | 역할 | 항목 수 | 출처 |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 1 | `editor-spec.json (인라인)` |
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
`states.groups` 를 두지 않은 것은 이 플러그인의 설정 화면에 **상태 변종이 없기**
때문입니다. 값이 있든 없든 같은 폼이 그려지므로, 상태를 나눠도 편집기에서 보이는 화면이
달라지지 않습니다.
동의 항목이 여러 개로 늘거나 항목별로 화면이 갈라지는 날이 오면 그때 `states` 를
신설합니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 `componentPalette` 를 선언하지 않습니다 — 편집기 팔레트에 추가되는 항목이 없습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일입니다. 모듈·플러그인은 레이아웃 JSON 에서 템플릿이
제공하는 컴포넌트를 **쓰기만** 하므로, 편집기 팔레트에 새로 얹을 것이 없습니다. 그래서 이
확장의 스펙은 `componentPalette`·`controls`·`componentCapabilities`·`nesting` 을 비우고
**도메인 데이터**(`sampleData`·`states`)만 담습니다.
팔레트에 무언가를 추가하고 싶다면 그것은 이 확장이 아니라 활성 템플릿
(`sirsoft-admin_basic` / `sirsoft-basic`)의 스펙에 가야 합니다. 여기에 팔레트를 선언하면
템플릿 선언과 같은 자리를 두고 다투게 되고, 어느 쪽이 이기는지가 합본 순서에 좌우됩니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 자리 | 역할 | 개수 | ID |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 1 | `marketing_settings` |
| `sampleData.byEndpointPattern` | 엔드포인트 패턴으로 붙는 프리뷰 응답 | 미선언 | - |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 미선언 | - |
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
마케팅 동의는 회원가입 폼·마이페이지 등 **다른 확장이 소유한 화면**에도 얹힙니다.
그 자리들은 레이아웃 확장 조각으로 주입되므로 이 스펙이 아니라 그 화면을 소유한 쪽의
샘플로 그려집니다 — 여기 `sampleData` 가 하나뿐인 이유입니다.
이 플러그인이 주입한 조각이 편집기에서 비어 보인다면 고칠 자리는 여기가 아니라
그 화면을 소유한 확장의 스펙입니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
편집기 스펙은 JSON 이므로 빌드가 필요 없습니다. 다만 편집기 서빙은 **활성 디렉토리만** 읽으므로(`_bundled` 폴백 없음) 편집 후 반드시 반영합니다:
```bash
php artisan plugin:update sirsoft-marketing --force
```
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
위 표는 "무엇을 함께 고치는가" 만 말합니다. 실제로 놓치는 자리는 **반영 절차**입니다 —
편집기가 읽는 것은 활성 디렉토리이고 `_bundled` 폴백이 없으므로, `_bundled` 에서 스펙을
고치고 update 커맨드를 돌리지 않으면 편집기에는 **직전 내용이 그대로 보입니다.** 파일은
고쳤는데 화면이 안 바뀌었다면 거의 이 경우입니다.
또 하나는 검증 시점입니다. 편집기 스펙은 스키마 검증을 통과해도 "레이아웃이 실제로 쓰는
ID 와 맞는가" 는 확인해 주지 않습니다. 그 어긋남은 편집기 캔버스에서만 빈 화면으로
나타나고 실제 화면은 정상이므로, 위 "샘플 데이터와 페이지 상태" 절의 미커버 목록이 유일한
통로입니다.
<!-- @intent END -->
@@ -160,6 +160,7 @@ HookManager::addAction(
- [ ] 레이아웃 조각 7개는 대상 화면(알림 설정·알림 템플릿 편집·발송 이력)의 자리가 사라지면 오류 없이 빠진다 — 코어·게시판·이커머스 업그레이드 후 노출 확인
- [ ] 크리덴셜 설정을 추가한다면 `frontend_schema` 에 `expose: false` + `sensitive: true` 를 함께 선언
- [ ] `dist/` 는 커밋되는 배포 산출물 — TS 를 고쳤으면 `--production` 재빌드 후 커밋 (`sourceMappingURL` 잔존 금지)
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 를 확인 — 이 확장은 편집기 스펙이 없어 `bizppurioCategories` · `bizppurioProfiles` · `bizppurio_templates_list` · `report_url` · `templates_readiness` 가 편집기 캔버스에서 빈 화면으로 보인다. `data_source` 를 더 늘리면 그 자리도 같은 상태가 된다
## 6. 금지 패턴
@@ -216,6 +217,7 @@ npx playwright test plugins/_bundled/sirsoft-message_bizppurio/tests/Playwright/
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [1.0.0] - 2026-08-24
@@ -1,13 +1,13 @@
# 비즈뿌리오 메시지 발송
**G7 플러그인 · sirsoft-message_bizppurio**
**그누보드7 플러그인 · sirsoft-message_bizppurio**
비즈뿌리오 연동 SMS/LMS·카카오 알림톡 발송 플러그인입니다. 코어 알림 시스템 채널로 문자·알림톡을 발송하고 발송 결과를 webhook 으로 수신합니다.
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.0.1-0066FF?style=flat-square" alt="version 1.0.1">
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.6-1F883D?style=flat-square" alt="G7 &gt;=7.0.6">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.6-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.6">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
</p>
<!-- @generated:badges END -->
@@ -23,7 +23,7 @@
<!-- @intent START -->
비즈뿌리오(Bizppurio)를 연동해 **문자(SMS/LMS)와 카카오 알림톡**을 발송하는 플러그인입니다.
G7 코어 알림 시스템에 문자·알림톡 채널을 추가하므로, 회원가입·주문 완료처럼 코어와 모듈이
그누보드7 코어 알림 시스템에 문자·알림톡 채널을 추가하므로, 회원가입·주문 완료처럼 코어와 모듈이
이미 발화하는 알림을 문자와 알림톡으로도 자동 발송할 수 있습니다. 알림을 새로 만들 필요 없이
**기존 알림의 채널만 켜면** 됩니다.
@@ -98,7 +98,7 @@ flowchart LR
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.6` |
| 그누보드7 코어 | `>=7.0.6` |
| PHP | `^8.2` |
<!-- @generated:requirements END -->
@@ -259,6 +259,7 @@ https://your-domain.com/api/plugins/sirsoft-message_bizppurio/webhook
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -33,7 +33,7 @@
| PHONE | body | string | 아니오 | max 20 | 수신 전화번호 |
| MEDIA | body | string | 아니오 | max 10 | 실제 발송된 매체 유형(대체발송 시 요청 유형과 다를 수 있음) |
| RESULT | body | string | 예 | max 10 | 발송 결과 코드 — ResultCodeResolver 가 성공/실패와 사유로 해석 |
| REFKEY | body | string | 예 | max 32 | 발송 시 G7 이 부여한 참조 키 — 이 값으로 bizppurio_dispatches 행을 찾는다 |
| REFKEY | body | string | 예 | max 32 | 발송 시 그누보드7 이 부여한 참조 키 — 이 값으로 bizppurio_dispatches 행을 찾는다 |
| TELRES | body | string | 아니오 | max 10 | 대체발송(문자) 결과 코드 |
| KAORES | body | string | 아니오 | max 10 | 알림톡 발송 결과 코드 |
@@ -0,0 +1,83 @@
# 비즈뿌리오 메시지 발송 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙(`editor-spec.json`)을 두지 않습니다. 편집기는 코어 기본 팔레트와 활성 템플릿의 스펙만으로 이 확장의 화면을 다룹니다._
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
문자·알림톡 채널 플러그인은 라우트 21개에 관리자 화면도 여럿 갖지만 편집기 스펙이
없습니다. 이것은 설계가 아니라 **아직 만들지 않은 상태**입니다 — 아래 미커버 목록이 그
결과를 보여 줍니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_선언된 편집기 스펙 블록이 없습니다._
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
선언한 블록이 없습니다. 이 플러그인의 관리자 화면은 연동 설정·알림톡 템플릿 관리 등
여러 갈래이고 각각이 자기 도메인 데이터를 읽으므로, 공용 ID 만으로는 덮이지 않습니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 팔레트에 항목을 추가하지 않습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일이므로, 이 확장이 팔레트에 얹을 것은 원래 없습니다.
편집기 팔레트는 활성 템플릿의 스펙이 정합니다 — 이 확장에 편집기 스펙이 생기더라도
`componentPalette` 는 여전히 비어 있을 것입니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙을 두지 않아 선언된 샘플 데이터·페이지 상태가 없습니다._
**프리뷰 샘플이 없는 `data_source` 5개** — 편집기 캔버스에서 이 자리만 빈 화면이 됩니다. 실제 화면은 정상이라 오류도 경고도 남지 않습니다.
`bizppurioCategories` · `bizppurioProfiles` · `bizppurio_templates_list` · `report_url` · `templates_readiness`
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
미커버가 5종으로 저장소에서 가장 많습니다 — `bizppurioCategories` ·
`bizppurioProfiles` · `bizppurio_templates_list` · `report_url` · `templates_readiness`.
알림톡 템플릿 관리 화면 전체가 편집기 캔버스에서 빈 채로 보인다는 뜻입니다. 이 플러그인의
관리자 화면을 편집기로 손보려는 운영자는 지금 목록도 상태도 볼 수 없습니다.
연동 설정 쪽 `settings` 만 공용 ID 라 템플릿 스펙이 채웁니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 아직 편집기 스펙을 두지 않습니다. 아래 변경이 생기면 `editor-spec.json` 을 신설합니다._
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
편집기 스펙을 신설해야 하는 시점은 하나입니다 — **이 확장이 소유한 레이아웃에
이 확장만 쓰는 `data_source` 가 생겼을 때**. 그 순간부터 편집기 캔버스의 그 영역은
빈 화면이 되고, 실제 화면은 정상 동작하므로 오류도 경고도 남지 않습니다.
신설 절차는 확장 루트에 `editor-spec.json` 을 만들고 `sampleData.byDataSourceId` 에
그 ID 를 넣는 것으로 시작합니다. 팔레트·컨트롤은 템플릿의 일이므로 넣지 않습니다.
파일을 만든 뒤 update 커맨드로 활성 디렉토리에 반영해야 편집기가 읽습니다 —
`_bundled` 폴백이 없습니다.
<!-- @intent END -->
@@ -120,6 +120,7 @@ CBT 인증 URL 로 폼 POST → KG 이니시스가 `sid` 를 콜백으로 전달
- [ ] 승인/취소 흐름을 고칠 때 `before_*`/`after_*` 훅 순서와 우선순위(`PaymentRefundListener` < `CancelActivityLogListener`)를 유지 — 로그가 실제 처리보다 먼저 실행되면 안 된다
- [ ] IP 화이트리스트(`InicisNotifyIpWhitelist`) 대상 라우트를 추가/변경하면 미들웨어 부착 대상(targets)도 함께 갱신
- [ ] 새 결제수단·통화를 추가하면 그 결제수단의 콜백 URL을 관리자 설정 안내(README "콜백/통보 URL 등록")에도 반영
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 의 동반 의무 표를 따라 `editor-spec.json` 을 함께 갱신 — 샘플이 없는 `data_source` 는 편집기 캔버스에서만 빈 화면이 되고 실제 화면은 정상이라 오류도 경고도 남지 않는다. 반영은 `php artisan plugin:update sirsoft-pay_kginicis --force`
## 6. 금지 패턴
@@ -168,6 +169,7 @@ cd plugins/_bundled/sirsoft-pay_kginicis && powershell -Command "npm run test:ru
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [1.1.2] - 2026-08-22
@@ -1,13 +1,13 @@
# KG 이니시스
**G7 플러그인 · sirsoft-pay_kginicis**
**그누보드7 플러그인 · sirsoft-pay_kginicis**
KG 이니시스 표준결제를 sirsoft-ecommerce 에 연결하는 결제 플러그인
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.1.3-0066FF?style=flat-square" alt="version 1.1.3">
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.10-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
<img src="https://img.shields.io/badge/requires-sirsoft--ecommerce-BF8700?style=flat-square" alt="requires sirsoft-ecommerce">
</p>
@@ -22,7 +22,7 @@ KG 이니시스 표준결제를 sirsoft-ecommerce 에 연결하는 결제 플러
## 소개
<!-- @intent START -->
KG 이니시스 표준결제를 G7 `sirsoft-ecommerce` 모듈에 연결하는 결제 플러그인입니다. PC 결제는
KG 이니시스 표준결제를 그누보드7 `sirsoft-ecommerce` 모듈에 연결하는 결제 플러그인입니다. PC 결제는
`INIStdPay.js` 표준결제창을, 모바일 결제는 모바일 표준결제창으로 이동한 뒤 서버 승인 API로
최종 승인하는 흐름을 씁니다. 일본 엔(JPY) 결제는 별도의 KG 이니시스 CBT(JPPG) 흐름을 씁니다.
@@ -85,7 +85,7 @@ CBT 결제창으로 진입하며, 설정이 부족하면 한국 표준결제로
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.10` |
| 그누보드7 코어 | `>=7.0.10` |
| PHP | `^8.2` |
| 의존 모듈 | `sirsoft-ecommerce` `>=1.1.0` |
<!-- @generated:requirements END -->
@@ -250,6 +250,7 @@ IP와 `devcbt.inicis.com` 443 연결 상태를 먼저 확인합니다.
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -0,0 +1,117 @@
# KG 이니시스 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| manifest | `plugins/_bundled/sirsoft-pay_kginicis/editor-spec.json` |
| 형태 | 단일 파일 (인라인) |
| 스펙 버전 | `1.0.0` |
| 스타일 시스템 | - |
| 다크 모드 전략 | - |
> KG이니시스 결제 플러그인 레이아웃 편집기 샘플 데이터 (가상계좌 입금통보 URL).
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
KG이니시스 결제 플러그인이 소유한 화면은 관리자 설정 하나뿐입니다. 결제 자체는 PG 결제창에서
일어나고 그 창은 이 확장이 그리는 화면이 아니므로, 편집기가 다룰 표면이 설정 화면으로
좁혀집니다.
`스타일 시스템`·`다크 모드 전략` 이 비어 있는 것은 정상입니다 — 화면을 그리는 규칙은
템플릿이 정하고, 결제 플러그인은 그 위에 자기 설정 항목만 얹습니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 블록 | 역할 | 항목 수 | 출처 |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 1 | `editor-spec.json (인라인)` |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 1 | `editor-spec.json (인라인)` |
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
선언한 것은 `vbank_info` 하나, 그리고 설정 화면의 상태 변종 하나입니다. 다른 설정 항목
(`settings` 등)은 여러 확장이 공유하는 공용 ID 라 admin 템플릿 스펙이 채웁니다 —
여기에 다시 선언하면 같은 ID 의 샘플이 두 곳에 생기고, 둘이 갈라져도 오류가 나지
않습니다.
가상계좌 안내(`vbank_info`)를 굳이 넣은 것은 그 영역이 **결제 수단에 따라 나타났다
사라지는** 자리라, 샘플이 없으면 편집기에서 그 영역 자체를 볼 수 없기 때문입니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 `componentPalette` 를 선언하지 않습니다 — 편집기 팔레트에 추가되는 항목이 없습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일입니다. 모듈·플러그인은 레이아웃 JSON 에서 템플릿이
제공하는 컴포넌트를 **쓰기만** 하므로, 편집기 팔레트에 새로 얹을 것이 없습니다. 그래서 이
확장의 스펙은 `componentPalette`·`controls`·`componentCapabilities`·`nesting` 을 비우고
**도메인 데이터**(`sampleData`·`states`)만 담습니다.
팔레트에 무언가를 추가하고 싶다면 그것은 이 확장이 아니라 활성 템플릿
(`sirsoft-admin_basic` / `sirsoft-basic`)의 스펙에 가야 합니다. 여기에 팔레트를 선언하면
템플릿 선언과 같은 자리를 두고 다투게 되고, 어느 쪽이 이기는지가 합본 순서에 좌우됩니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 자리 | 역할 | 개수 | ID |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 1 | `vbank_info` |
| `sampleData.byEndpointPattern` | 엔드포인트 패턴으로 붙는 프리뷰 응답 | 미선언 | - |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 1 | `*/admin/plugins/sirsoft-pay_kginicis/settings` |
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
상태 변종의 범위가 `*/admin/plugins/sirsoft-pay_kginicis/settings` 하나인 것은 이 플러그인이 자기
설정 화면만 소유하기 때문입니다. 주문·결제 화면은 `sirsoft-ecommerce` 가 소유하므로
그 화면의 프리뷰는 이커머스 스펙이 그립니다.
결제 흐름을 편집기에서 확인하려던 것이라면 이 문서가 아니라 이커머스 모듈의 편집기 스펙
문서를 봅니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
편집기 스펙은 JSON 이므로 빌드가 필요 없습니다. 다만 편집기 서빙은 **활성 디렉토리만** 읽으므로(`_bundled` 폴백 없음) 편집 후 반드시 반영합니다:
```bash
php artisan plugin:update sirsoft-pay_kginicis --force
```
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
위 표는 "무엇을 함께 고치는가" 만 말합니다. 실제로 놓치는 자리는 **반영 절차**입니다 —
편집기가 읽는 것은 활성 디렉토리이고 `_bundled` 폴백이 없으므로, `_bundled` 에서 스펙을
고치고 update 커맨드를 돌리지 않으면 편집기에는 **직전 내용이 그대로 보입니다.** 파일은
고쳤는데 화면이 안 바뀌었다면 거의 이 경우입니다.
또 하나는 검증 시점입니다. 편집기 스펙은 스키마 검증을 통과해도 "레이아웃이 실제로 쓰는
ID 와 맞는가" 는 확인해 주지 않습니다. 그 어긋남은 편집기 캔버스에서만 빈 화면으로
나타나고 실제 화면은 정상이므로, 위 "샘플 데이터와 페이지 상태" 절의 미커버 목록이 유일한
통로입니다.
이 플러그인의 결제 수단이 이커머스 관리자 화면에 어떻게 보이는지는 편집기 스펙이 아니라
결제수단 카탈로그 선언(`needs_pg`·`pg_provider`·`pg_locked`)이 정합니다. 배지가 이상하게
보인다면 스펙이 아니라 그 선언을 봅니다.
<!-- @intent END -->
@@ -124,6 +124,7 @@ OS 판별 후 `executeCliWindows()`/`executeCliLinux()` 로 분기 → CLI 인
- [ ] 승인/취소 흐름을 고칠 때 `before_*`/`after_*` 훅 순서와 우선순위(`PaymentRefundListener` < `CancelActivityLogListener`)를 유지 — 로그가 실제 처리보다 먼저 실행되면 안 된다
- [ ] IP 화이트리스트(`RestrictKcpIp`) 대상 라우트를 추가/변경하면 미들웨어 부착 대상(targets)도 함께 갱신
- [ ] 새 결제수단·통화를 추가하면 그 결제수단의 콜백 URL을 관리자 설정 안내(README "콜백 및 통보 URL")에도 반영
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 의 동반 의무 표를 따라 `editor-spec.json` 을 함께 갱신 — 샘플이 없는 `data_source` 는 편집기 캔버스에서만 빈 화면이 되고 실제 화면은 정상이라 오류도 경고도 남지 않는다. 반영은 `php artisan plugin:update sirsoft-pay_nhnkcp --force`
## 6. 금지 패턴
@@ -172,6 +173,7 @@ cd plugins/_bundled/sirsoft-pay_nhnkcp && powershell -Command "npm run test:run
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [1.0.3] - 2026-08-22
@@ -1,13 +1,13 @@
# NHN KCP
**G7 플러그인 · sirsoft-pay_nhnkcp**
**그누보드7 플러그인 · sirsoft-pay_nhnkcp**
NHN KCP Standard Pay 결제를 sirsoft-ecommerce 에 연결하는 결제 플러그인
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.0.4-0066FF?style=flat-square" alt="version 1.0.4">
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.10-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
<img src="https://img.shields.io/badge/requires-sirsoft--ecommerce-BF8700?style=flat-square" alt="requires sirsoft-ecommerce">
</p>
@@ -22,7 +22,7 @@ NHN KCP Standard Pay 결제를 sirsoft-ecommerce 에 연결하는 결제 플러
## 소개
<!-- @intent START -->
NHN KCP Standard Pay 결제를 G7 `sirsoft-ecommerce` 모듈에 연결하는 결제 플러그인입니다. PC
NHN KCP Standard Pay 결제를 그누보드7 `sirsoft-ecommerce` 모듈에 연결하는 결제 플러그인입니다. PC
결제는 `payplus_web.jsp` 결제창 + 서버의 KCP CLI 승인 모듈을, 모바일 결제는 SmartPhone Pay
SOAP 승인키 발급 + 모바일 결제창을 씁니다.
@@ -83,7 +83,7 @@ PC 결제 승인은 `NhnKcpApiService`가 OS 를 판별해 `pp_cli`/`pp_cli_x64`
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.10` |
| 그누보드7 코어 | `>=7.0.10` |
| PHP | `^8.2` |
| 의존 모듈 | `sirsoft-ecommerce` `>=1.1.0` |
<!-- @generated:requirements END -->
@@ -254,6 +254,7 @@ php artisan plugin:update sirsoft-pay_nhnkcp --force
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -0,0 +1,117 @@
# NHN KCP — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| manifest | `plugins/_bundled/sirsoft-pay_nhnkcp/editor-spec.json` |
| 형태 | 단일 파일 (인라인) |
| 스펙 버전 | `1.0.0` |
| 스타일 시스템 | - |
| 다크 모드 전략 | - |
> NHN KCP 결제 플러그인 레이아웃 편집기 샘플 데이터 (가상계좌 입금통보 URL / 연동 상태 점검).
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
NHN KCP 결제 플러그인이 소유한 화면은 관리자 설정 하나뿐입니다. 결제 자체는 PG 결제창에서
일어나고 그 창은 이 확장이 그리는 화면이 아니므로, 편집기가 다룰 표면이 설정 화면으로
좁혀집니다.
`스타일 시스템`·`다크 모드 전략` 이 비어 있는 것은 정상입니다 — 화면을 그리는 규칙은
템플릿이 정하고, 결제 플러그인은 그 위에 자기 설정 항목만 얹습니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 블록 | 역할 | 항목 수 | 출처 |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 2 | `editor-spec.json (인라인)` |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 1 | `editor-spec.json (인라인)` |
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
선언한 것은 `vbank_info` 와 연동 점검용 `health` 둘, 그리고 설정 화면의 상태 변종 하나입니다. 다른 설정 항목
(`settings` 등)은 여러 확장이 공유하는 공용 ID 라 admin 템플릿 스펙이 채웁니다 —
여기에 다시 선언하면 같은 ID 의 샘플이 두 곳에 생기고, 둘이 갈라져도 오류가 나지
않습니다.
가상계좌 안내(`vbank_info`)를 굳이 넣은 것은 그 영역이 **결제 수단에 따라 나타났다
사라지는** 자리라, 샘플이 없으면 편집기에서 그 영역 자체를 볼 수 없기 때문입니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 `componentPalette` 를 선언하지 않습니다 — 편집기 팔레트에 추가되는 항목이 없습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일입니다. 모듈·플러그인은 레이아웃 JSON 에서 템플릿이
제공하는 컴포넌트를 **쓰기만** 하므로, 편집기 팔레트에 새로 얹을 것이 없습니다. 그래서 이
확장의 스펙은 `componentPalette`·`controls`·`componentCapabilities`·`nesting` 을 비우고
**도메인 데이터**(`sampleData`·`states`)만 담습니다.
팔레트에 무언가를 추가하고 싶다면 그것은 이 확장이 아니라 활성 템플릿
(`sirsoft-admin_basic` / `sirsoft-basic`)의 스펙에 가야 합니다. 여기에 팔레트를 선언하면
템플릿 선언과 같은 자리를 두고 다투게 되고, 어느 쪽이 이기는지가 합본 순서에 좌우됩니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 자리 | 역할 | 개수 | ID |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 2 | `vbank_info` · `health` |
| `sampleData.byEndpointPattern` | 엔드포인트 패턴으로 붙는 프리뷰 응답 | 미선언 | - |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 1 | `*/admin/plugins/sirsoft-pay_nhnkcp/settings` |
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
상태 변종의 범위가 `*/admin/plugins/sirsoft-pay_nhnkcp/settings` 하나인 것은 이 플러그인이 자기
설정 화면만 소유하기 때문입니다. 주문·결제 화면은 `sirsoft-ecommerce` 가 소유하므로
그 화면의 프리뷰는 이커머스 스펙이 그립니다.
결제 흐름을 편집기에서 확인하려던 것이라면 이 문서가 아니라 이커머스 모듈의 편집기 스펙
문서를 봅니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
편집기 스펙은 JSON 이므로 빌드가 필요 없습니다. 다만 편집기 서빙은 **활성 디렉토리만** 읽으므로(`_bundled` 폴백 없음) 편집 후 반드시 반영합니다:
```bash
php artisan plugin:update sirsoft-pay_nhnkcp --force
```
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
위 표는 "무엇을 함께 고치는가" 만 말합니다. 실제로 놓치는 자리는 **반영 절차**입니다 —
편집기가 읽는 것은 활성 디렉토리이고 `_bundled` 폴백이 없으므로, `_bundled` 에서 스펙을
고치고 update 커맨드를 돌리지 않으면 편집기에는 **직전 내용이 그대로 보입니다.** 파일은
고쳤는데 화면이 안 바뀌었다면 거의 이 경우입니다.
또 하나는 검증 시점입니다. 편집기 스펙은 스키마 검증을 통과해도 "레이아웃이 실제로 쓰는
ID 와 맞는가" 는 확인해 주지 않습니다. 그 어긋남은 편집기 캔버스에서만 빈 화면으로
나타나고 실제 화면은 정상이므로, 위 "샘플 데이터와 페이지 상태" 절의 미커버 목록이 유일한
통로입니다.
이 플러그인의 결제 수단이 이커머스 관리자 화면에 어떻게 보이는지는 편집기 스펙이 아니라
결제수단 카탈로그 선언(`needs_pg`·`pg_provider`·`pg_locked`)이 정합니다. 배지가 이상하게
보인다면 스펙이 아니라 그 선언을 봅니다.
<!-- @intent END -->
@@ -117,6 +117,7 @@ API 와 동기화하는 역할만 합니다. 등록은 훅 기반입니다
- [ ] 인증 실패(1단계)와 승인 실패(2단계)의 사용자 안내 방식(silent redirect vs `?error=`)을 구분 유지 — §1 "의도적으로 하지 않는 것" 참고
- [ ] IP 화이트리스트(`VbankNotifyIpWhitelist`) 대상 라우트를 추가/변경하면 미들웨어 부착 대상(targets)도 함께 갱신
- [ ] 새 간편결제 수단을 추가하면 그 결제수단의 계약 상태를 관리자 안내에도 반영
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 의 동반 의무 표를 따라 `editor-spec.json` 을 함께 갱신 — 샘플이 없는 `data_source` 는 편집기 캔버스에서만 빈 화면이 되고 실제 화면은 정상이라 오류도 경고도 남지 않는다. 반영은 `php artisan plugin:update sirsoft-pay_nicepayments --force`
## 6. 금지 패턴
@@ -164,6 +165,7 @@ cd plugins/_bundled/sirsoft-pay_nicepayments && powershell -Command "npm run tes
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [1.0.2] - 2026-08-19
@@ -1,13 +1,13 @@
# NicePayments
**G7 플러그인 · sirsoft-pay_nicepayments**
**그누보드7 플러그인 · sirsoft-pay_nicepayments**
나이스페이먼츠 결제를 sirsoft-ecommerce 에 연결하는 결제 플러그인
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.0.3-0066FF?style=flat-square" alt="version 1.0.3">
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.10-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
<img src="https://img.shields.io/badge/requires-sirsoft--ecommerce-BF8700?style=flat-square" alt="requires sirsoft-ecommerce">
</p>
@@ -22,7 +22,7 @@
## 소개
<!-- @intent START -->
나이스페이먼츠(NicePayments) 표준결제를 G7 `sirsoft-ecommerce` 모듈에 연결하는 결제
나이스페이먼츠(NicePayments) 표준결제를 그누보드7 `sirsoft-ecommerce` 모듈에 연결하는 결제
플러그인입니다. 결제 승인은 **인증→승인 2단계**로 나뉩니다 — 결제창(`goPay` iframe
팝업/모바일 폼)이 먼저 인증 결과를 서버로 보내고, 서버가 그 결과를 받아 별도 승인 API를
호출해야 최종 완료됩니다.
@@ -76,7 +76,7 @@ URL로 결과를 POST 하면 결제 완료 처리됩니다.
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.10` |
| 그누보드7 코어 | `>=7.0.10` |
| PHP | `^8.2` |
| 의존 모듈 | `sirsoft-ecommerce` `>=1.1.0` |
<!-- @generated:requirements END -->
@@ -212,6 +212,7 @@ https://your-domain.com/plugins/sirsoft-pay_nicepayments/payment/vbank-notify
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -0,0 +1,117 @@
# 나이스페이먼츠 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| manifest | `plugins/_bundled/sirsoft-pay_nicepayments/editor-spec.json` |
| 형태 | 단일 파일 (인라인) |
| 스펙 버전 | `1.0.0` |
| 스타일 시스템 | - |
| 다크 모드 전략 | - |
> 나이스페이먼츠 결제 플러그인 레이아웃 편집기 샘플 데이터 (가상계좌 입금통보 URL).
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
나이스페이먼츠 결제 플러그인이 소유한 화면은 관리자 설정 하나뿐입니다. 결제 자체는 PG 결제창에서
일어나고 그 창은 이 확장이 그리는 화면이 아니므로, 편집기가 다룰 표면이 설정 화면으로
좁혀집니다.
`스타일 시스템`·`다크 모드 전략` 이 비어 있는 것은 정상입니다 — 화면을 그리는 규칙은
템플릿이 정하고, 결제 플러그인은 그 위에 자기 설정 항목만 얹습니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 블록 | 역할 | 항목 수 | 출처 |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 1 | `editor-spec.json (인라인)` |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 1 | `editor-spec.json (인라인)` |
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
선언한 것은 `vbank_info` 하나, 그리고 설정 화면의 상태 변종 하나입니다. 다른 설정 항목
(`settings` 등)은 여러 확장이 공유하는 공용 ID 라 admin 템플릿 스펙이 채웁니다 —
여기에 다시 선언하면 같은 ID 의 샘플이 두 곳에 생기고, 둘이 갈라져도 오류가 나지
않습니다.
가상계좌 안내(`vbank_info`)를 굳이 넣은 것은 그 영역이 **결제 수단에 따라 나타났다
사라지는** 자리라, 샘플이 없으면 편집기에서 그 영역 자체를 볼 수 없기 때문입니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 `componentPalette` 를 선언하지 않습니다 — 편집기 팔레트에 추가되는 항목이 없습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일입니다. 모듈·플러그인은 레이아웃 JSON 에서 템플릿이
제공하는 컴포넌트를 **쓰기만** 하므로, 편집기 팔레트에 새로 얹을 것이 없습니다. 그래서 이
확장의 스펙은 `componentPalette`·`controls`·`componentCapabilities`·`nesting` 을 비우고
**도메인 데이터**(`sampleData`·`states`)만 담습니다.
팔레트에 무언가를 추가하고 싶다면 그것은 이 확장이 아니라 활성 템플릿
(`sirsoft-admin_basic` / `sirsoft-basic`)의 스펙에 가야 합니다. 여기에 팔레트를 선언하면
템플릿 선언과 같은 자리를 두고 다투게 되고, 어느 쪽이 이기는지가 합본 순서에 좌우됩니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 자리 | 역할 | 개수 | ID |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 1 | `vbank_info` |
| `sampleData.byEndpointPattern` | 엔드포인트 패턴으로 붙는 프리뷰 응답 | 미선언 | - |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 1 | `*/admin/plugins/sirsoft-pay_nicepayments/settings` |
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
상태 변종의 범위가 `*/admin/plugins/sirsoft-pay_nicepayments/settings` 하나인 것은 이 플러그인이 자기
설정 화면만 소유하기 때문입니다. 주문·결제 화면은 `sirsoft-ecommerce` 가 소유하므로
그 화면의 프리뷰는 이커머스 스펙이 그립니다.
결제 흐름을 편집기에서 확인하려던 것이라면 이 문서가 아니라 이커머스 모듈의 편집기 스펙
문서를 봅니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
편집기 스펙은 JSON 이므로 빌드가 필요 없습니다. 다만 편집기 서빙은 **활성 디렉토리만** 읽으므로(`_bundled` 폴백 없음) 편집 후 반드시 반영합니다:
```bash
php artisan plugin:update sirsoft-pay_nicepayments --force
```
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
위 표는 "무엇을 함께 고치는가" 만 말합니다. 실제로 놓치는 자리는 **반영 절차**입니다 —
편집기가 읽는 것은 활성 디렉토리이고 `_bundled` 폴백이 없으므로, `_bundled` 에서 스펙을
고치고 update 커맨드를 돌리지 않으면 편집기에는 **직전 내용이 그대로 보입니다.** 파일은
고쳤는데 화면이 안 바뀌었다면 거의 이 경우입니다.
또 하나는 검증 시점입니다. 편집기 스펙은 스키마 검증을 통과해도 "레이아웃이 실제로 쓰는
ID 와 맞는가" 는 확인해 주지 않습니다. 그 어긋남은 편집기 캔버스에서만 빈 화면으로
나타나고 실제 화면은 정상이므로, 위 "샘플 데이터와 페이지 상태" 절의 미커버 목록이 유일한
통로입니다.
이 플러그인의 결제 수단이 이커머스 관리자 화면에 어떻게 보이는지는 편집기 스펙이 아니라
결제수단 카탈로그 선언(`needs_pg`·`pg_provider`·`pg_locked`)이 정합니다. 배지가 이상하게
보인다면 스펙이 아니라 그 선언을 봅니다.
<!-- @intent END -->
@@ -126,6 +126,7 @@
- [ ] 가상계좌 웹훅의 secret 대조(`webhook_secret_verify`)를 기본값 `true` 이외로 바꾸지 않는다 — 끄면 토스 노티 위조를 막을 수단이 사라진다
- [ ] `order_sheet_mode` 관련 로직을 고칠 때 `RegisterPgProviderListener`(enabled_methods)와 `RegisterTossPaymentMethodsListener`(builtin 결제수단 주입) 양쪽을 함께 갱신 — 한쪽만 고치면 설정과 노출 목록이 어긋난다
- [ ] `ValidateTossSettingsListener`에 새 범위 검증을 추가하면 `core.plugin_settings.before_save` 의 `sync: true`를 유지
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 를 확인 — 이 확장은 편집기 스펙이 없어도 되는 상태(공용 ID 만 사용)다. 이 확장만 쓰는 `data_source` 를 새로 붙이는 순간 `editor-spec.json` 신설이 필요해진다
## 6. 금지 패턴
@@ -173,6 +174,7 @@ cd plugins/_bundled/sirsoft-tosspayments && powershell -Command "npm run test:ru
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [1.0.2] - 2026-08-19
@@ -1,13 +1,13 @@
# 토스페이먼츠
**G7 플러그인 · sirsoft-tosspayments**
**그누보드7 플러그인 · sirsoft-tosspayments**
토스페이먼츠 결제 게이트웨이 (통합결제창 연동)
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.0.3-0066FF?style=flat-square" alt="version 1.0.3">
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.10-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
<img src="https://img.shields.io/badge/requires-sirsoft--ecommerce-BF8700?style=flat-square" alt="requires sirsoft-ecommerce">
</p>
@@ -22,7 +22,7 @@
## 소개
<!-- @intent START -->
토스페이먼츠 통합결제창 결제를 G7 `sirsoft-ecommerce` 모듈에 연결하는 결제 플러그인입니다.
토스페이먼츠 통합결제창 결제를 그누보드7 `sirsoft-ecommerce` 모듈에 연결하는 결제 플러그인입니다.
승인은 브라우저 리다이렉트 기반입니다 — 결제창(SDK)이 결제를 처리한 뒤 브라우저를 콜백
URL로 돌려보내고, 서버가 그 파라미터로 승인 확인 API를 호출합니다.
@@ -72,7 +72,7 @@ flowchart LR
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.10` |
| 그누보드7 코어 | `>=7.0.10` |
| PHP | `^8.2` |
| 의존 모듈 | `sirsoft-ecommerce` `>=1.1.0` |
<!-- @generated:requirements END -->
@@ -188,6 +188,7 @@ https://your-domain.com/plugins/sirsoft-tosspayments/webhook/deposit
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -86,7 +86,7 @@ Location: /shop/checkout?error=PAY_PROCESS_CANCELED&orderId=20260711-000001
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
| --- | --- | --- | --- | --- | --- |
| paymentKey | query | string | 예 | — | 토스가 발급한 결제 키. Confirm API 호출에 사용한다. |
| orderId | query | string | 예 | — | 주문번호 (G7 `orders.order_number`). SDK 호출 시 넘긴 값이 그대로 돌아온다. |
| orderId | query | string | 예 | — | 주문번호 (그누보드7 `orders.order_number`). SDK 호출 시 넘긴 값이 그대로 돌아온다. |
| amount | query | integer | 예 | min 1 | 결제 금액. 주문의 결제요청 금액과 대조하며, 불일치 시 결제를 승인하지 않는다. |
**요청 예시**
@@ -27,7 +27,7 @@
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
| --- | --- | --- | --- | --- | --- |
| orderId | body | string | 예 | max 100 | 주문번호 (G7 `orders.order_number`). 이 값으로 주문·결제 레코드를 조회한다. |
| orderId | body | string | 예 | max 100 | 주문번호 (그누보드7 `orders.order_number`). 이 값으로 주문·결제 레코드를 조회한다. |
| status | body | string | 예 | `DONE`, `CANCELED` | 입금 결과. `DONE`=입금완료 → 결제완료 처리, `CANCELED`=입금취소 → 결제실패 처리. |
| secret | body | string | 아니오 | max 255 | 결제 승인 응답에서 발급받아 `payment_meta.toss_secret` 에 저장해 둔 값. 위조 방지 대조에 사용. |
| transactionKey | body | string | 아니오 | max 255 | 토스 거래 키. 결제완료 처리 시 `transaction_id` 로 기록한다 (없으면 기존 값 유지). |
@@ -111,7 +111,7 @@ OK
| eventType | body | string | 아니오 | max 64 | 토스 이벤트 종류 (예: `PAYMENT_STATUS_CHANGED`). |
| createdAt | body | string | 아니오 | max 64 | 토스가 이벤트를 생성한 시각. |
| data | body | array | 예 | — | 결제 정보 객체. `data.orderId`(주문번호)와 `data.status`(토스 결제상태)를 읽어 로컬 상태와 대조한다. |
| data.orderId | body | string | 예 | max 100 | 주문번호 (G7 `orders.order_number`). 이 값으로 주문·결제 레코드를 조회해 로컬 결제상태를 확인한다. |
| data.orderId | body | string | 예 | max 100 | 주문번호 (그누보드7 `orders.order_number`). 이 값으로 주문·결제 레코드를 조회해 로컬 결제상태를 확인한다. |
| data.status | body | string | 예 | max 40 | 토스 측 결제상태 (예: `DONE`, `CANCELED`, `WAITING_FOR_DEPOSIT`). 로컬 `payments.payment_status` 와 함께 로그에 기록해 불일치를 추적한다. |
| data.paymentKey | body | string | 아니오 | max 255 | 토스 결제 키. 수신만 하며 이 엔드포인트에서 상태 전이에 사용하지 않는다. |
@@ -0,0 +1,83 @@
# 토스페이먼츠 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙(`editor-spec.json`)을 두지 않습니다. 편집기는 코어 기본 팔레트와 활성 템플릿의 스펙만으로 이 확장의 화면을 다룹니다._
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
토스페이먼츠 결제 플러그인은 다른 결제 3종과 달리 편집기 스펙을 두지 않았습니다.
그럼에도 미커버가 없는 것은 이 플러그인의 설정 화면이 공용 ID 만 읽기 때문입니다 —
다른 결제 플러그인이 선언한 `vbank_info` 에 해당하는 영역을 이 플러그인은 설정 화면에서
같은 방식으로 다루지 않습니다.
즉 지금은 스펙 없이도 편집기에서 화면이 온전히 보입니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_선언된 편집기 스펙 블록이 없습니다._
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
선언한 블록이 없습니다. 형제 결제 플러그인(`sirsoft-pay_kginicis` 등)은 스펙을 갖고
있으므로, 이 플러그인에 가상계좌 안내 같은 도메인 영역을 추가할 때는 그쪽 스펙을 선례로
봅니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 팔레트에 항목을 추가하지 않습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일이므로, 이 확장이 팔레트에 얹을 것은 원래 없습니다.
편집기 팔레트는 활성 템플릿의 스펙이 정합니다 — 이 확장에 편집기 스펙이 생기더라도
`componentPalette` 는 여전히 비어 있을 것입니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙을 두지 않아 선언된 샘플 데이터·페이지 상태가 없습니다._
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
미커버가 없습니다. 설정 화면이 읽는 `settings` 는 공용 ID 라 admin 템플릿 스펙이
채우므로, 편집기에서 설정 화면이 값이 채워진 상태로 보입니다.
결제 흐름 화면(주문·결제·완료)은 `sirsoft-ecommerce` 가 소유하므로 그 프리뷰는 이커머스
스펙이 그립니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 아직 편집기 스펙을 두지 않습니다. 아래 변경이 생기면 `editor-spec.json` 을 신설합니다._
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
편집기 스펙을 신설해야 하는 시점은 하나입니다 — **이 확장이 소유한 레이아웃에
이 확장만 쓰는 `data_source` 가 생겼을 때**. 그 순간부터 편집기 캔버스의 그 영역은
빈 화면이 되고, 실제 화면은 정상 동작하므로 오류도 경고도 남지 않습니다.
신설 절차는 확장 루트에 `editor-spec.json` 을 만들고 `sampleData.byDataSourceId` 에
그 ID 를 넣는 것으로 시작합니다. 팔레트·컨트롤은 템플릿의 일이므로 넣지 않습니다.
파일을 만든 뒤 update 커맨드로 활성 디렉토리에 반영해야 편집기가 읽습니다 —
`_bundled` 폴백이 없습니다.
<!-- @intent END -->
@@ -134,6 +134,7 @@ Cache 에 stash(`inicis:pending_record:{key}`) → 회원가입 완료 → `core
- [ ] PII 컬럼(이름·생년월일·성별·CI/DI 등)을 다루는 코드 변경 시 GDPR 삭제/탈퇴 정리 리스너(`CleanInicisRecordOnUserDelete`/`CleanInicisRecordOnUserWithdraw`)가 여전히 그 컬럼을 정리하는지 확인
- [ ] 팝업 기반 인증 흐름(`startAuth`)을 고칠 때 `window.open`을 사용자 클릭 컨텍스트 밖으로 옮기지 않는다 — Chrome popup blocker 회피가 깨진다
- [ ] `duplicate_field`/`duplicate_block_enabled` 로직을 고치면 `InicisDuplicateField` Enum 과 `AssertNoDuplicateInicisIdentity`를 함께 갱신
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 의 동반 의무 표를 따라 `editor-spec.json` 을 함께 갱신 — 샘플이 없는 `data_source` 는 편집기 캔버스에서만 빈 화면이 되고 실제 화면은 정상이라 오류도 경고도 남지 않는다. 반영은 `php artisan plugin:update sirsoft-verification_kginicis --force`
## 6. 금지 패턴
@@ -182,6 +183,7 @@ cd plugins/_bundled/sirsoft-verification_kginicis && powershell -Command "npm ru
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [1.0.4] - 2026-08-22
@@ -1,13 +1,13 @@
# KG이니시스 본인인증
**G7 플러그인 · sirsoft-verification_kginicis**
KG이니시스 통합인증의 본인확인(reqSvcCd=03)을 G7 코어 IDV 인프라에 Provider 로 등록하는 플러그인
**그누보드7 플러그인 · sirsoft-verification_kginicis**
KG이니시스 통합인증의 본인확인(reqSvcCd=03)을 그누보드7 코어 IDV 인프라에 Provider 로 등록하는 플러그인
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.0.5-0066FF?style=flat-square" alt="version 1.0.5">
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.8-1F883D?style=flat-square" alt="G7 &gt;=7.0.8">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.8-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.8">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
</p>
<!-- @generated:badges END -->
@@ -21,7 +21,7 @@ KG이니시스 통합인증의 본인확인(reqSvcCd=03)을 G7 코어 IDV 인프
## 소개
<!-- @intent START -->
KG이니시스 본인확인(휴대폰 인증, reqSvcCd=03)을 G7 코어의 본인인증(IDV) 체계에 연결하는
KG이니시스 본인확인(휴대폰 인증, reqSvcCd=03)을 그누보드7 코어의 본인인증(IDV) 체계에 연결하는
플러그인입니다. 코어가 정의한 표준 인터페이스를 구현해, 회원가입·비밀번호 찾기·민감작업
등 코어가 IDV 를 요구하는 모든 지점에서 이메일 인증 대신 이니시스 팝업이 동작하게
합니다.
@@ -70,7 +70,7 @@ flowchart LR
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.8` |
| 그누보드7 코어 | `>=7.0.8` |
| PHP | `^8.2` |
<!-- @generated:requirements END -->
@@ -158,6 +158,7 @@ provider 를 쓸지 선택합니다.
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -0,0 +1,113 @@
# KG이니시스 본인인증 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| manifest | `plugins/_bundled/sirsoft-verification_kginicis/editor-spec.json` |
| 형태 | 단일 파일 (인라인) |
| 스펙 버전 | `1.0.0` |
| 스타일 시스템 | - |
| 다크 모드 전략 | - |
> KG이니시스 본인인증 플러그인 레이아웃 편집기 샘플 데이터.
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
KG이니시스 통합인증 Provider 플러그인은 코어 IDV 인프라에 자기 Provider 를 등록하는 것이 본체이고,
화면은 관리자 설정과 **사용자 화면에 뜨는 인증 창**입니다. 그래서 스펙이 담는 것도 그
두 자리뿐입니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 블록 | 역할 | 항목 수 | 출처 |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 1 | `editor-spec.json (인라인)` |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 2 | `editor-spec.json (인라인)` |
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
`byDataSourceId` 는 `inicisRecord` 하나입니다 — 인증 결과 레코드를 화면에 보여 주는 자리입니다.
인증 정책·목적·메시지는 코어 IDV 가 소유하고 admin 템플릿 스펙이 그 샘플을 채우므로
여기서 다시 선언하지 않습니다.
`states.groups` 가 2종인 것이 이 플러그인의 특징입니다. 인증은 **여러 화면에 걸쳐
나타나는 기능**이라 설정 화면 하나로 끝나지 않습니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 `componentPalette` 를 선언하지 않습니다 — 편집기 팔레트에 추가되는 항목이 없습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일입니다. 모듈·플러그인은 레이아웃 JSON 에서 템플릿이
제공하는 컴포넌트를 **쓰기만** 하므로, 편집기 팔레트에 새로 얹을 것이 없습니다. 그래서 이
확장의 스펙은 `componentPalette`·`controls`·`componentCapabilities`·`nesting` 을 비우고
**도메인 데이터**(`sampleData`·`states`)만 담습니다.
팔레트에 무언가를 추가하고 싶다면 그것은 이 확장이 아니라 활성 템플릿
(`sirsoft-admin_basic` / `sirsoft-basic`)의 스펙에 가야 합니다. 여기에 팔레트를 선언하면
템플릿 선언과 같은 자리를 두고 다투게 되고, 어느 쪽이 이기는지가 합본 순서에 좌우됩니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 자리 | 역할 | 개수 | ID |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 1 | `inicisRecord` |
| `sampleData.byEndpointPattern` | 엔드포인트 패턴으로 붙는 프리뷰 응답 | 미선언 | - |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 2 | `*/admin/plugins/sirsoft-verification_kginicis/settings` · `_user_base` |
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
상태 범위는 `*/admin/plugins/sirsoft-verification_kginicis/settings` 와 `_user_base` 입니다. `_user_base` 가 들어 있는 것이 핵심입니다 — 본인인증
요구는 특정 라우트가 아니라 **어느 화면에서든 428 응답으로 발생**할 수 있고, 그때 뜨는
인증 창은 베이스 레이아웃 위에 얹힙니다.
편집기 캔버스는 실제 428 응답을 받지 않으므로, 그 상태를 변종으로 주입해 두지 않으면
인증 창이 화면에 나타나지 않아 **편집할 방법이 없습니다.**
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
편집기 스펙은 JSON 이므로 빌드가 필요 없습니다. 다만 편집기 서빙은 **활성 디렉토리만** 읽으므로(`_bundled` 폴백 없음) 편집 후 반드시 반영합니다:
```bash
php artisan plugin:update sirsoft-verification_kginicis --force
```
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
위 표는 "무엇을 함께 고치는가" 만 말합니다. 실제로 놓치는 자리는 **반영 절차**입니다 —
편집기가 읽는 것은 활성 디렉토리이고 `_bundled` 폴백이 없으므로, `_bundled` 에서 스펙을
고치고 update 커맨드를 돌리지 않으면 편집기에는 **직전 내용이 그대로 보입니다.** 파일은
고쳤는데 화면이 안 바뀌었다면 거의 이 경우입니다.
또 하나는 검증 시점입니다. 편집기 스펙은 스키마 검증을 통과해도 "레이아웃이 실제로 쓰는
ID 와 맞는가" 는 확인해 주지 않습니다. 그 어긋남은 편집기 캔버스에서만 빈 화면으로
나타나고 실제 화면은 정상이므로, 위 "샘플 데이터와 페이지 상태" 절의 미커버 목록이 유일한
통로입니다.
인증 창의 모양을 바꿨다면 이 스펙만으로는 끝나지 않습니다. 인증 창을 여는 주체는
템플릿 부트스트랩이 등록한 launcher 이므로, launcher 가 여는 화면과 여기 상태 변종이
가리키는 화면이 같은지 함께 확인합니다.
<!-- @intent END -->
@@ -132,6 +132,7 @@ FormRequest 검증과 Service 저장이 서로 다른 파이프라인 단계이
- [ ] 데스크톱/모바일 분기(`isMobileEnvironment()`)를 고칠 때 양쪽 복귀 경로(팝업 종료 감지 / `redirectStash` 복원)를 함께 테스트
- [ ] `duplicate_field`/`duplicate_block_enabled` 로직을 고치면 `KcpDuplicateField` Enum 과 `AssertNoDuplicateKcpIdentity`를 함께 갱신
- [ ] `normalizeLiveSiteCd()`/`addLiveModeRules()`를 고칠 때 두 훅(`filter_save_data`/`update_validation_rules`)의 실행 순서(검증 먼저, 정규화는 저장 시점)를 유지
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 의 동반 의무 표를 따라 `editor-spec.json` 을 함께 갱신 — 샘플이 없는 `data_source` 는 편집기 캔버스에서만 빈 화면이 되고 실제 화면은 정상이라 오류도 경고도 남지 않는다. 반영은 `php artisan plugin:update sirsoft-verification_nhnkcp --force`
## 6. 금지 패턴
@@ -184,6 +185,7 @@ npx playwright test plugins/_bundled/sirsoft-verification_nhnkcp/tests/Playwrigh
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -9,6 +9,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [1.0.1] - 2026-08-19
@@ -1,13 +1,13 @@
# NHN KCP 휴대폰 본인확인
**G7 플러그인 · sirsoft-verification_nhnkcp**
NHN KCP 휴대폰 본인확인(V2 REST)을 G7 코어 IDV 인프라에 Provider 로 등록하는 플러그인
**그누보드7 플러그인 · sirsoft-verification_nhnkcp**
NHN KCP 휴대폰 본인확인(V2 REST)을 그누보드7 코어 IDV 인프라에 Provider 로 등록하는 플러그인
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.0.2-0066FF?style=flat-square" alt="version 1.0.2">
<img src="https://img.shields.io/badge/type-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-555555?style=flat-square" alt="type 플러그인">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.6-1F883D?style=flat-square" alt="G7 &gt;=7.0.6">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.6-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.6">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
</p>
<!-- @generated:badges END -->
@@ -21,7 +21,7 @@ NHN KCP 휴대폰 본인확인(V2 REST)을 G7 코어 IDV 인프라에 Provider
## 소개
<!-- @intent START -->
NHN KCP 휴대폰 본인확인을 G7 코어의 본인인증(IDV) 체계에 연결하는 플러그인입니다.
NHN KCP 휴대폰 본인확인을 그누보드7 코어의 본인인증(IDV) 체계에 연결하는 플러그인입니다.
`sirsoft-verification_kginicis`와 같은 부류(코어 표준 인터페이스 구현)지만 다른 벤더이며,
운영자는 둘 중 하나 또는 둘 다 설치해 사용할 수 있습니다.
@@ -73,7 +73,7 @@ flowchart LR
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.6` |
| 그누보드7 코어 | `>=7.0.6` |
| PHP | `^8.2` |
<!-- @generated:requirements END -->
@@ -161,6 +161,7 @@ php artisan plugin:update sirsoft-verification_nhnkcp --force
| [docs/data-model.md](docs/data-model.md) | 모델·소유 테이블·마이그레이션·Enum | ✅ |
| [docs/settings.md](docs/settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 | ✅ |
| [docs/frontend.md](docs/frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [docs/api/](docs/api/README.md) | API 레퍼런스 (엔드포인트별 파라미터·응답 필드) | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -16,6 +16,7 @@
| [data-model.md](data-model.md) | 모델·소유 테이블·마이그레이션·Enum |
| [settings.md](settings.md) | 설정 스키마·권한·메뉴·라우트·의존 관계 |
| [frontend.md](frontend.md) | 레이아웃·액션 핸들러·전역 진입점·에셋 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [api/](api/README.md) | API 레퍼런스 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
@@ -0,0 +1,113 @@
# NHN KCP 휴대폰 본인확인 — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| manifest | `plugins/_bundled/sirsoft-verification_nhnkcp/editor-spec.json` |
| 형태 | 단일 파일 (인라인) |
| 스펙 버전 | `1.0.0` |
| 스타일 시스템 | - |
| 다크 모드 전략 | - |
> NHN KCP 휴대폰 본인확인 플러그인 레이아웃 편집기 샘플 데이터.
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
NHN KCP 휴대폰 본인확인 Provider 플러그인은 코어 IDV 인프라에 자기 Provider 를 등록하는 것이 본체이고,
화면은 관리자 설정과 **사용자 화면에 뜨는 인증 창**입니다. 그래서 스펙이 담는 것도 그
두 자리뿐입니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 블록 | 역할 | 항목 수 | 출처 |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 1 | `editor-spec.json (인라인)` |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 3 | `editor-spec.json (인라인)` |
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
`byDataSourceId` 는 `nhnkcpRecord` 하나입니다 — 인증 결과 레코드를 화면에 보여 주는 자리입니다.
인증 정책·목적·메시지는 코어 IDV 가 소유하고 admin 템플릿 스펙이 그 샘플을 채우므로
여기서 다시 선언하지 않습니다.
`states.groups` 가 3종인 것이 이 플러그인의 특징입니다. 인증은 **여러 화면에 걸쳐
나타나는 기능**이라 설정 화면 하나로 끝나지 않습니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 `componentPalette` 를 선언하지 않습니다 — 편집기 팔레트에 추가되는 항목이 없습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일입니다. 모듈·플러그인은 레이아웃 JSON 에서 템플릿이
제공하는 컴포넌트를 **쓰기만** 하므로, 편집기 팔레트에 새로 얹을 것이 없습니다. 그래서 이
확장의 스펙은 `componentPalette`·`controls`·`componentCapabilities`·`nesting` 을 비우고
**도메인 데이터**(`sampleData`·`states`)만 담습니다.
팔레트에 무언가를 추가하고 싶다면 그것은 이 확장이 아니라 활성 템플릿
(`sirsoft-admin_basic` / `sirsoft-basic`)의 스펙에 가야 합니다. 여기에 팔레트를 선언하면
템플릿 선언과 같은 자리를 두고 다투게 되고, 어느 쪽이 이기는지가 합본 순서에 좌우됩니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 자리 | 역할 | 개수 | ID |
|---|---|---|---|
| `sampleData.byDataSourceId` | 레이아웃 `data_sources` ID 로 붙는 프리뷰 응답 | 1 | `nhnkcpRecord` |
| `sampleData.byEndpointPattern` | 엔드포인트 패턴으로 붙는 프리뷰 응답 | 미선언 | - |
| `states.groups` | 상태 변종을 적용할 범위(라우트·베이스 레이아웃) | 3 | `*/mypage/profile` · `*/admin/plugins/sirsoft-verification_nhnkcp/settings` · `_user_base` |
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
상태 범위는 `*/mypage/profile` · `*/admin/plugins/sirsoft-verification_nhnkcp/settings` · `_user_base` 입니다. `_user_base` 가 들어 있는 것이 핵심입니다 — 본인인증
요구는 특정 라우트가 아니라 **어느 화면에서든 428 응답으로 발생**할 수 있고, 그때 뜨는
인증 창은 베이스 레이아웃 위에 얹힙니다.
편집기 캔버스는 실제 428 응답을 받지 않으므로, 그 상태를 변종으로 주입해 두지 않으면
인증 창이 화면에 나타나지 않아 **편집할 방법이 없습니다.**
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
편집기 스펙은 JSON 이므로 빌드가 필요 없습니다. 다만 편집기 서빙은 **활성 디렉토리만** 읽으므로(`_bundled` 폴백 없음) 편집 후 반드시 반영합니다:
```bash
php artisan plugin:update sirsoft-verification_nhnkcp --force
```
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
위 표는 "무엇을 함께 고치는가" 만 말합니다. 실제로 놓치는 자리는 **반영 절차**입니다 —
편집기가 읽는 것은 활성 디렉토리이고 `_bundled` 폴백이 없으므로, `_bundled` 에서 스펙을
고치고 update 커맨드를 돌리지 않으면 편집기에는 **직전 내용이 그대로 보입니다.** 파일은
고쳤는데 화면이 안 바뀌었다면 거의 이 경우입니다.
또 하나는 검증 시점입니다. 편집기 스펙은 스키마 검증을 통과해도 "레이아웃이 실제로 쓰는
ID 와 맞는가" 는 확인해 주지 않습니다. 그 어긋남은 편집기 캔버스에서만 빈 화면으로
나타나고 실제 화면은 정상이므로, 위 "샘플 데이터와 페이지 상태" 절의 미커버 목록이 유일한
통로입니다.
인증 창의 모양을 바꿨다면 이 스펙만으로는 끝나지 않습니다. 인증 창을 여는 주체는
템플릿 부트스트랩이 등록한 launcher 이므로, launcher 가 여는 화면과 여기 상태 변종이
가리키는 화면이 같은지 함께 확인합니다.
<!-- @intent END -->
@@ -116,6 +116,7 @@ SSoT 는 코어 `config/template.php` 의 `required_admin_components` 입니다.
- [ ] TSX 를 고쳤다면 `template:build --production` 후 `dist/` 동반 커밋 (`sourceMappingURL` 잔존 금지)
- [ ] `docs/extension/sample-extensions.md` 의 계층 표와 어긋나지 않는지 확인
- [ ] 레이아웃 렌더링 테스트(`__tests__/layouts/*.test.tsx`)는 이 템플릿 디렉토리에 둔다 — 코어 디렉토리에 두지 않는다
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 를 확인 — 이 확장은 편집기 스펙이 없어도 되는 상태(공용 ID 만 사용)다. 이 확장만 쓰는 `data_source` 를 새로 붙이는 순간 `editor-spec.json` 신설이 필요해진다
## 6. 금지 패턴
@@ -161,5 +162,6 @@ cd templates/_bundled/gnuboard7-hello_admin_template && powershell -Command "npm
| [docs/components.md](docs/components.md) | 템플릿이 제공하는 컴포넌트 | ✅ |
| [docs/layouts.md](docs/layouts.md) | 레이아웃 목록과 라우트 매핑 | ✅ |
| [docs/handlers.md](docs/handlers.md) | 템플릿 전용 핸들러와 부트스트랩 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -10,6 +10,8 @@
- 아이콘(Font Awesome)을 템플릿에 함께 담아 외부 CDN 없이 동작합니다.
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [0.1.0] - 2026-07-01
@@ -1,13 +1,13 @@
# Hello Admin Template
**G7 템플릿 · gnuboard7-hello_admin_template**
**그누보드7 템플릿 · gnuboard7-hello_admin_template**
그누보드7 학습용 최소 Admin 템플릿 스켈레톤
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-0.1.1-0066FF?style=flat-square" alt="version 0.1.1">
<img src="https://img.shields.io/badge/type-%ED%85%9C%ED%94%8C%EB%A6%BF-555555?style=flat-square" alt="type 템플릿">
<img src="https://img.shields.io/badge/G7-%3E%3D7.0.10-1F883D?style=flat-square" alt="G7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/%EA%B7%B8%EB%88%84%EB%B3%B4%EB%93%9C7-%3E%3D7.0.10-1F883D?style=flat-square" alt="그누보드7 &gt;=7.0.10">
<img src="https://img.shields.io/badge/license-MIT-8250DF?style=flat-square" alt="license MIT">
</p>
<!-- @generated:badges END -->
@@ -72,7 +72,7 @@ flowchart TD
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.10` |
| 그누보드7 코어 | `>=7.0.10` |
| PHP | `^8.2` |
<!-- @generated:requirements END -->
@@ -156,6 +156,7 @@ php artisan template:activate gnuboard7-hello_admin_template
| [docs/components.md](docs/components.md) | 템플릿이 제공하는 컴포넌트 | ✅ |
| [docs/layouts.md](docs/layouts.md) | 레이아웃 목록과 라우트 매핑 | ✅ |
| [docs/handlers.md](docs/handlers.md) | 템플릿 전용 핸들러와 부트스트랩 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -15,6 +15,7 @@
| [components.md](components.md) | 템플릿이 제공하는 컴포넌트 |
| [layouts.md](layouts.md) | 레이아웃 목록과 라우트 매핑 |
| [handlers.md](handlers.md) | 템플릿 전용 핸들러와 부트스트랩 |
| [editor-spec.md](editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
<!-- @generated:doc-toc END -->
@@ -0,0 +1,85 @@
# Hello Admin Template — 레이아웃 편집기 스펙
> 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 · 진입점: [AGENTS.md](../AGENTS.md)
## 선언 요약
<!-- @generated:editor-spec-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙(`editor-spec.json`)을 두지 않습니다. 편집기는 코어 기본 팔레트와 활성 템플릿의 스펙만으로 이 확장의 화면을 다룹니다._
<!-- @generated:editor-spec-summary END -->
<!-- @intent START -->
학습용 관리자 템플릿이라 편집기 스펙을 두지 않았습니다. 이 템플릿의 목적은 관리자
템플릿의 최소 구조(레이아웃·라우트·베이스 레이아웃)를 보여 주는 것이고, 편집기 스펙은
그 위에 얹히는 별개 축입니다.
실제 관리자 템플릿이 스펙으로 무엇을 선언하는지는 `sirsoft-admin_basic` 의 같은 문서를
봅니다 — 팔레트 79 · 컨트롤 303 · 역량 86 이 그 규모입니다.
<!-- @intent END -->
## 선언 블록
<!-- @generated:editor-spec-blocks START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_선언된 편집기 스펙 블록이 없습니다._
<!-- @generated:editor-spec-blocks END -->
<!-- @intent START -->
선언한 블록이 없습니다. 팔레트·컨트롤·역량·중첩을 선언하지 않았으므로 이 템플릿을
활성화한 상태에서는 편집기가 다룰 컴포넌트 어휘가 없습니다.
이것이 "템플릿이 편집기 스펙을 갖는다" 는 규율의 의미입니다 — 스펙은 편집기의 부가
기능이 아니라 **편집기가 그 템플릿에서 동작하기 위한 어휘 자체**입니다.
<!-- @intent END -->
## 컴포넌트 팔레트
<!-- @generated:editor-spec-palette START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 팔레트에 항목을 추가하지 않습니다._
<!-- @generated:editor-spec-palette END -->
<!-- @intent START -->
컴포넌트를 만드는 것은 템플릿의 일이므로, 이 확장이 팔레트에 얹을 것은 원래 없습니다.
편집기 팔레트는 활성 템플릿의 스펙이 정합니다 — 이 확장에 편집기 스펙이 생기더라도
`componentPalette` 는 여전히 비어 있을 것입니다.
<!-- @intent END -->
## 샘플 데이터와 페이지 상태
<!-- @generated:editor-spec-samples START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 편집기 스펙을 두지 않아 선언된 샘플 데이터·페이지 상태가 없습니다._
_이 확장 레이아웃의 `data_source` 는 전부 프리뷰 샘플이 붙습니다 (이 확장 또는 번들 템플릿 스펙이 커버)._
<!-- @generated:editor-spec-samples END -->
<!-- @intent START -->
미커버가 없습니다. 이 템플릿의 레이아웃에는 `data_source` 자체가 없기 때문입니다 —
정적 화면만으로 구성된 학습용 골격이라 붙일 데이터가 없습니다.
미커버 0 이 곧 "편집기에서 온전히 보인다" 를 뜻하지는 않습니다. 여기서는 그릴 데이터가
애초에 없다는 뜻입니다.
<!-- @intent END -->
## 수정 시 동반 의무
<!-- @generated:editor-spec-obligations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_이 확장은 아직 편집기 스펙을 두지 않습니다. 아래 변경이 생기면 `editor-spec.json` 을 신설합니다._
| 이런 변경을 했다면 | 편집기 스펙에서 함께 할 일 |
|---|---|
| 컴포넌트를 새로 만들었다 | `componentPalette` 에 항목 추가 · `componentCapabilities` 에 편집 역량 선언 · `nesting` 에 담길 자리 규정 |
| 레이아웃에 `data_sources` 를 추가했다 | `sampleData` 에 같은 ID 로 프리뷰 응답 추가 (없으면 편집기 캔버스만 빈 화면) |
| `_global.*` 을 새로 읽는다 | `sampleGlobal` 에 baseline 값 추가 |
| 빈 목록·오류 같은 화면 변종을 추가했다 | `states` 에 변종 추가 · `stateLabels` 에 친화 명칭 |
| 새 액션·조건 패턴을 도입했다 | `actionRecipes` / `conditionRecipes` 에 친화 명칭 등록 |
<!-- @generated:editor-spec-obligations END -->
<!-- @intent START -->
이 템플릿을 실제 사용 템플릿으로 발전시킨다면 편집기 스펙 신설이 **가장 먼저** 필요한
작업 중 하나입니다. 템플릿의 스펙은 모듈·플러그인과 달리 도메인 데이터가 아니라
**컴포넌트 어휘**(팔레트·컨트롤·역량·중첩)를 담기 때문입니다.
신설 순서는 `componentPalette` → `nesting` → `componentCapabilities` → `controls`
입니다. 앞의 둘이 없으면 편집기에서 컴포넌트를 놓을 수조차 없고, 뒤의 둘은 놓은 다음에
속성을 바꾸기 위한 것입니다. `sirsoft-admin_basic/editor-spec/` 의 13개 블록 파일이
완성된 형태의 선례입니다.
<!-- @intent END -->
@@ -23,7 +23,7 @@
**홈 화면이 이 샘플의 핵심**입니다. `data_sources` 로 학습용 모듈의 메모 API
(`/api/modules/gnuboard7-hello_module/memos`)를 호출해 목록을 그립니다 — 모듈이 데이터를,
템플릿이 화면을 담당하는 G7 의 기본 경계를 가장 짧게 보여주는 예시입니다. 실제 게시판·이커머스
템플릿이 화면을 담당하는 그누보드7 의 기본 경계를 가장 짧게 보여주는 예시입니다. 실제 게시판·이커머스
모듈도 방문자 화면을 갖지 않고 같은 방식으로 템플릿에 맡깁니다.
`manifest.hidden = true` 라 관리자 UI 의 템플릿 목록에 나타나지 않습니다. artisan CLI 로는
@@ -117,6 +117,7 @@ User 템플릿은 자기가 그리고 싶은 화면에 필요한 API 를 `data_s
- [ ] `manifest.hidden = true` 를 유지 (복제본에서만 제거)
- [ ] TSX 를 고쳤다면 `template:build --production` 후 `dist/` 동반 커밋 (`sourceMappingURL` 잔존 금지)
- [ ] 레이아웃 렌더링 테스트(`__tests__/layouts/*.test.tsx`)는 이 템플릿 디렉토리에 둔다
- [ ] 레이아웃·컴포넌트·`data_source` 를 건드렸다면 [`docs/editor-spec.md`](docs/editor-spec.md) 를 확인 — 이 확장은 편집기 스펙이 없어 `memos` 가 편집기 캔버스에서 빈 화면으로 보인다. `data_source` 를 더 늘리면 그 자리도 같은 상태가 된다
## 6. 금지 패턴
@@ -163,5 +164,6 @@ cd templates/_bundled/gnuboard7-hello_user_template && powershell -Command "npm
| [docs/components.md](docs/components.md) | 템플릿이 제공하는 컴포넌트 | ✅ |
| [docs/layouts.md](docs/layouts.md) | 레이아웃 목록과 라우트 매핑 | ✅ |
| [docs/handlers.md](docs/handlers.md) | 템플릿 전용 핸들러와 부트스트랩 | ✅ |
| [docs/editor-spec.md](docs/editor-spec.md) | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -10,6 +10,8 @@
- 아이콘(Font Awesome)을 템플릿에 함께 담아 외부 CDN 없이 동작합니다.
- 개발자와 AI 에이전트를 위한 문서를 추가했습니다. 확장 폴더의 `AGENTS.md`(설계 의도·확장점·수정 시 확인할 것)와 `README.md`(도입·운영 안내), `docs/`(상세 문서)로 구성됩니다.
- 확장 문서에 「레이아웃 편집기 스펙」 항목을 추가했습니다. 이 확장이 레이아웃 편집기에 무엇을 선언했는지와, 화면 요소나 데이터를 추가할 때 편집기 쪽에서 함께 해야 할 일을 담습니다.
- 문서의 제품 표기를 「그누보드7」로 통일했습니다.
## [0.1.0] - 2026-07-01

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