docs(core,extensions): 확장 개발자 문서 9세트 집필과 생성기 정합 보강

S1 파일럿 3종(sirsoft-board · sirsoft-gdpr · sirsoft-admin_basic)에 이어
결제·본인인증 동형군 6종의 AGENTS.md · README.md · docs/ 5문서를 집필했다.
확장을 고치려는 쪽이 매번 src/ 를 훑어 구조를 재발견하지 않도록, 설계 의도와
확장점(발행·구독 훅)·수정 시 동반 의무·금지 패턴을 코드 근거로 서술했다.

생성기(ExtensionDocScaffolder)에서 표를 무의미하게 만들던 세 결함을 함께 고쳤다.
getLayoutExtensions 기본 구현이 돌려주는 절대경로가 파일 목록과 중복돼 로컬
머신 경로가 커밋 문서에 실리던 문제, getNotificationDefinitions 를 'key'/'event'
로 읽어 모든 행이 '-' 로 찍히던 문제, getSettingsLayout 절대경로가 정규화 없이
노출되던 문제다. 셋 다 예외를 남기지 않고 표만 조용히 망가뜨리므로
ExtensionDocContractTest 에 각각의 되돌림 red 를 확인한 단언을 두었다.
템플릿의 extensions/{id}/ 는 모듈·플러그인의 발행과 반대 방향(오버라이드)이라
별도 블록(template-overrides)으로 분리했다.

sirsoft-admin_basic 의 컴포넌트·핸들러·레이아웃 문서를 코어 docs/ 에서 그 템플릿
소유로 이관하고, 남은 참조 6축을 재는 가드를 추가했다. 이관 후 남은 옛 경로는
오류가 아니라 헛걸음으로만 나타나 드러나지 않는다. 의 상대 링크가
한 단계 얕아 공유 docs/ 대신 를 가리키던 문제도 함께 고쳤다.

README 상단의 확장명 이미지 배지를 평문 H1 로 바꿨다( 지시 2026-08-31).
루트 README.md · README.ko.md 도 같은 기준을 적용했다. 정보 배지는 유지한다.

sirsoft-gdpr: 회원탈퇴로 자동 철회된 동의가 관리자 동의 이력 화면의 출처 필터로
걸러지지 않던 문제를 고쳤다. Repository 가 'withdraw' 리터럴을 직접 UPDATE 에
싣는데 그 값이 ConsentSource enum 에 없어, 화면 필터 옵션·라벨 어느 쪽에도
도달하지 못했다. 어휘를 enum 단일 출처로 모으고 ko·en·ja 라벨과 필터 옵션을
함께 채웠으며, 어휘 대조 테스트의 모집단에 Repository 를 편입했다.
This commit is contained in:
HeuJung
2026-08-31 15:57:44 +09:00
parent 11175d35d6
commit 8328b1db77
113 changed files with 12002 additions and 1554 deletions
@@ -0,0 +1,150 @@
# Admin Basic — 에이전트 가이드
> 이 문서는 이 템플릿을 수정하는 에이전트·확장개발자를 위한 것입니다. 도입 검토·운영 관점은 [README.md](README.md) 를 보세요.
## TL;DR (5초 요약)
```text
1. 유형: 템플릿 (sirsoft-admin_basic) — 코어·모든 번들 확장의 관리자 화면을 그리는 유일한 admin 템플릿
2. 확장 방식: 필수 컴포넌트(35개, config/template.php)만 써서 레이아웃 JSON 작성 — 다른 admin 템플릿으로 교체돼도 동작 보장
3. 건드리면 안 되는 것: `Icon` 컴포넌트로 AdminSidebar 아이콘 렌더(반드시 `I`+FontAwesome 클래스), `_admin_base` 슬롯 구조를 임의로 재배치
4. 작업 위치: `templates/_bundled/sirsoft-admin_basic` — 활성 디렉토리 직접 수정 금지
5. 반영: `php artisan template:update sirsoft-admin_basic --force`
```
## 1. 이 확장은 무엇인가
<!-- @intent START -->
G7 이 기본 제공하는 유일한 admin 타입 템플릿입니다 — 코어 관리자 화면(대시보드·사용자·역할·
설정·확장 관리 등)뿐 아니라 **모든 번들 모듈/플러그인의 관리자 레이아웃**(`resources/layouts/
admin/`)이 이 템플릿의 베이스(`_admin_base`)를 extends 하고 이 템플릿의 컴포넌트로 그려집니다.
그래서 이 템플릿의 공개 계약(필수 컴포넌트 35개, `_admin_base` 슬롯 구조)을 깨면 코어가
아니라 **전체 번들 확장의 관리자 화면**이 동시에 영향을 받습니다 — 다른 번들 확장 하나를
고치는 것과는 파급 범위가 다릅니다.
**설계 원칙**: 모듈/플러그인 개발자가 "이 컴포넌트만 쓰면 다른 admin 템플릿으로 바꿔도
안전하다"는 보장을 받도록, 필수 컴포넌트 목록(config/template.php)을 이 템플릿 하나가 아니라
**admin 템플릿이라면 지켜야 할 계약**으로 취급합니다 — 이 템플릿에만 있는 편의 컴포넌트를
필수 목록에 넣지 않습니다.
**의도적으로 하지 않는 것**: 방문자용(user) 화면은 이 템플릿의 범위가 아닙니다(`sirsoft-basic`
소관). 또한 컴포넌트 Props 전체 레퍼런스는 이 문서가 다시 나열하지 않습니다 — 코어
`docs/frontend/component-props*.md` 가 SSoT 이고, 이 문서는 이 템플릿에서만 유효한 계약
(필수 컴포넌트·AdminSidebar·SlotContainer·베이스 레이아웃 구조)만 다룹니다.
<!-- @intent END -->
## 2. 디렉토리 지도
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 경로 | 역할 | 수정 시 필요한 절차 |
|---|---|---|
| `template.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json 동기화 |
| `routes.json` | 라우트 → 레이아웃 매핑 | `php artisan template:update sirsoft-admin_basic --force` |
| `layouts/` | 레이아웃 JSON | `php artisan template:update sirsoft-admin_basic --force` (빌드 불필요) |
| `src/components/` | React 컴포넌트 | `php artisan template:build` → `php artisan template:update sirsoft-admin_basic --force` |
| `src/handlers/` | 템플릿 전용 액션 핸들러 | `php artisan template:build` → `php artisan template:update sirsoft-admin_basic --force` |
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan template:update sirsoft-admin_basic --force` |
| `editor-spec/` | 분할 편집기 스펙 | `php artisan template:update sirsoft-admin_basic --force` |
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan template:update sirsoft-admin_basic --force` |
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
<!-- @generated:directory-map END -->
## 3. 핵심 흐름
<!-- @intent START -->
**새 관리자 화면 렌더**: 방문자가 `/admin/{path}` 접근 → `routes.json` 이 경로를 레이아웃
이름으로 매핑 → 그 레이아웃이 `extends: _admin_base` 로 사이드바·헤더·Toast·콘텐츠 슬롯을
상속 → `slots.content` 에 정의된 컴포넌트(대개 `PageHeader`+`DataGrid`/`Card`/`Form`
조합)가 데이터소스 API 를 호출해 화면을 채웁니다. 이 흐름은 코어 화면과 번들 확장 화면이
완전히 동일합니다 — 확장이 관리자 화면을 추가할 때 이 템플릿을 복제하지 않고 자기
`resources/layouts/admin/*.json` 에서 그대로 `_admin_base` 를 extends 합니다.
**사이드바 접힘 상태 복원**: 페이지 로드 → `src/index.ts` 부트스트랩이 `initSidebar()` 직접
호출(레이아웃 `init_actions` 아님) → localStorage 값을 `_global.sidebarCollapsed` 에 반영 →
`_admin_base.json` 의 사이드바 영역 className 표현식이 그 값을 읽어 접힘 스타일 적용.
**로케일 전환**: 코어가 로케일을 바꾸면 이 템플릿의 `initTemplate()` 재등록 진입점이 호출되어
핸들러 맵을 다시 등록합니다(§docs/handlers.md "부트스트랩"). 사이드바 접힘 등 1회성 부팅
작업은 이 진입점에 섞지 않습니다 — 로케일 전환마다 중복 실행되면 안 되기 때문입니다.
<!-- @intent END -->
## 4. 확장점
<!-- @generated:extension-points-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 확장점 | 수 | 상세 |
|---|---|---|
| 제공 컴포넌트 | 125개 | [제공 컴포넌트](docs/components.md#제공-컴포넌트) |
| 레이아웃 | 145개 | [레이아웃 목록](docs/layouts.md#레이아웃-목록) |
| 전용 핸들러 | 17개 | [템플릿 전용 핸들러](docs/handlers.md#템플릿-전용-핸들러) |
| 확장 오버라이드 | 0개 | [확장 오버라이드](docs/layouts.md#확장-오버라이드) |
<!-- @generated:extension-points-summary END -->
<!-- @intent START -->
이 템플릿은 훅을 발행/구독하지 않습니다 — 관리자 화면 확장은 훅이 아니라 **레이아웃 확장
오버라이드**(`extensions/{module-identifier}/*.json`, 지금은 0개)와 **컴포넌트 재사용**
(공통 컴포넌트를 그대로 쓰는 것) 두 경로로만 이뤄집니다. 다른 확장의 관리자 화면 UI 를
이 템플릿에서만 다르게 그리고 싶다면 첫 번째 경로를, 새 화면 유형(예: 새로운 카드 스타일)이
필요하면 `src/components/composite/` 에 컴포넌트를 추가하고 `components.json` 에 등록하는
쪽을 씁니다.
<!-- @intent END -->
## 5. 수정 시 동반 의무
- [ ] `_bundled` 에서만 수정하고 `php artisan template:update sirsoft-admin_basic --force` 로 반영
- [ ] manifest version 상향 시 `package.json` · `package-lock.json` 동기화 + CHANGELOG 기재
- [ ] 레이아웃 JSON 변경 시 빌드 없이 update 만 — 신규 Tailwind 클래스는 빌드된 CSS 에 존재하는지 확인
- [ ] TSX/TS 변경 시 `--production` 재빌드 후 `dist/` 커밋 (sourceMappingURL 잔존 금지)
- [ ] 다국어 키 추가 시 ko·en 동시 반영 + 번들 ja 언어팩 증분 동기화
- [ ] 필수 컴포넌트(35개) 의 Props 시그니처를 깨는 변경은 전체 번들 확장 관리자 화면에 영향 — 변경 전 `src/components/{basic,composite}/` 의 실사용처를 넓게 확인
- [ ] `_admin_base.json` 슬롯 구조(`content` 슬롯 등) 변경 시 그 슬롯에 의존하는 모든 화면(145개 레이아웃 대다수) 영향 검토
- [ ] AdminSidebar 의 `MenuItem`/`AdminSidebarProps` 인터페이스 확장 시 이 문서의 §docs/components.md "AdminSidebar 상세" 동기화
## 6. 금지 패턴
<!-- @intent START -->
| 금지 | 올바른 사용 | 이유 |
|---|---|---|
| `AdminSidebar` 메뉴 아이콘을 `Icon name="home"` 식으로 렌더 | `I` 컴포넌트 + FontAwesome 클래스 문자열(`icon` 필드가 이미 `"fas fa-home"` 형태) | 메뉴 API 가 클래스 문자열을 내려주므로 `Icon`(IconName enum) 으로 받으면 렌더되지 않는다 |
| 필수 컴포넌트 목록 밖의 이 템플릿 전용 컴포넌트를 모듈 레이아웃에서 사용 | 필수 컴포넌트(config/template.php) 만 사용 | 다른 admin 템플릿으로 교체 시 그 화면만 깨진다 |
| 사이드바 접힘 상태를 레이아웃 `init_actions` 로 매번 복원 | 템플릿 부트스트랩(`src/index.ts`)에서 1회 복원 | `init_actions` 는 화면 진입마다 재실행되어 불필요한 반복 처리가 된다 |
| `_admin_base` 를 상속하는데 로그인 화면처럼 `initTheme`/메뉴 초기화를 다시 호출 | `_admin_base` 상속 화면은 이미 초기화된 전역 상태를 그대로 사용 | 중복 호출은 낭비이며, 두 초기화 지점의 결과가 어긋나면 화면 간 상태 불일치가 생긴다 |
<!-- @intent END -->
## 7. 테스트 실행
<!-- @generated:test-commands START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 종류 | 개수 | 위치 |
|---|---|---|
| PHPUnit | 0개 | — |
| Vitest | 203개 | `vitest.config.ts` |
| Playwright | 8개 | `tests/Playwright` |
| 시나리오 매니페스트 | 2개 | `tests/scenarios` |
```bash
# Vitest (확장 디렉토리에서) (PowerShell)
cd templates/_bundled/sirsoft-admin_basic && powershell -Command "npm run test:run -- <대상>"
# Playwright E2E (Bash)
npx playwright test templates/_bundled/sirsoft-admin_basic/tests/Playwright/specs/<대상>.spec.ts
```
무필터 전체 실행은 금지되어 있습니다 — 변경 범위에 걸리는 대상만 지정해 실행합니다.
<!-- @generated:test-commands END -->
## 8. 문서 목차
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 문서 | 내용 | 상태 |
|---|---|---|
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
| [docs/components.md](docs/components.md) | 템플릿이 제공하는 컴포넌트 | ✅ |
| [docs/layouts.md](docs/layouts.md) | 레이아웃 목록과 라우트 매핑 | ✅ |
| [docs/handlers.md](docs/handlers.md) | 템플릿 전용 핸들러와 부트스트랩 | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
@@ -0,0 +1,180 @@
# Admin Basic
**G7 템플릿 · sirsoft-admin_basic**
그누보드7 기본 관리자 템플릿
<!-- @generated:badges START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
<p align="center">
<img src="https://img.shields.io/badge/version-1.0.8-0066FF?style=flat-square" alt="version 1.0.8">
<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/license-MIT-8250DF?style=flat-square" alt="license MIT">
</p>
<!-- @generated:badges END -->
---
[소개](#소개) · [주요 기능](#주요-기능) · [동작 방식](#동작-방식) · [요구 사항](#요구-사항) · [설치](#설치) · [제공 컴포넌트](#제공-컴포넌트) · [사용 방법](#사용-방법) · [다른 확장과의 연동](#다른-확장과의-연동) · [문서](#문서) · [트러블슈팅](#트러블슈팅) · [변경 이력](#변경-이력) · [라이선스](#라이선스)
---
## 소개
<!-- @intent START -->
G7 이 기본 제공하는 관리자(admin) 템플릿입니다. 코어 관리자 화면뿐 아니라 설치된 모든
모듈/플러그인의 관리자 화면이 이 템플릿의 컴포넌트와 베이스 레이아웃을 그대로 사용합니다 —
확장을 설치하면 그 확장의 관리자 UI 도 자동으로 이 템플릿의 디자인(사이드바·헤더·색상·다크
모드)을 따릅니다.
이 템플릿은 사용자(방문자)용 화면을 그리지 않습니다 — 방문자 화면은 `sirsoft-basic` 템플릿의
몫입니다. 또한 완전히 다른 디자인의 관리자 화면이 필요하면 이 템플릿을 고치는 대신 같은
컴포넌트 계약(필수 컴포넌트 35개)을 구현하는 새 admin 템플릿을 만드는 것이 원칙입니다 — 그래야
기존 확장들의 관리자 화면이 새 템플릿에서도 깨지지 않습니다.
<!-- @intent END -->
## 주요 기능
<!-- @intent START -->
| 영역 | 설명 |
|---|---|
| 관리자 셸 | 사이드바(계층형 메뉴, 접힘 지원)·헤더·다크 모드·다국어 전환을 갖춘 공통 화면 골격(`_admin_base`) |
| 컴포넌트 125개 | HTML 래핑 39개(basic) + UI 패턴 캡슐화 80개(composite) + 페이지 구조 5개(layout) + 모달 1개 |
| 필수 컴포넌트 계약 | 35개 컴포넌트만 사용하면 다른 admin 템플릿으로 교체해도 화면이 보장되는 모듈 호환성 기준 |
| 레이아웃 145개 | 대시보드·사용자·역할·메뉴·설정·확장 관리·스케줄·활동/알림 로그 등 코어 관리자 화면 전체 |
| 확장 관리 UI | 모듈/플러그인/템플릿 설치·활성화·업데이트·삭제와 레이아웃 편집기(코드 편집 + 실시간 미리보기) |
| 본인인증(IDV) 챌린지 | 관리자 민감 작업에 걸리는 본인인증 화면 |
<!-- @intent END -->
## 동작 방식
<!-- @intent START -->
```mermaid
flowchart TD
base["_admin_base (사이드바·헤더·Toast·콘텐츠 슬롯)"] --> list["목록 화면 (PageHeader+DataGrid+Pagination)"]
base --> detail["상세 화면 (PageHeader+Card)"]
base --> form["폼 화면 (Form+FormField)"]
base --> settings["설정 화면 (TabNavigation+_tab_*)"]
auth["인증 화면 (admin_login 등, _admin_base 미상속)"]
errors["에러 화면 (errors/*, 독립 레이아웃)"]
```
인증 화면(로그인/비밀번호 찾기·재설정)과 에러 화면은 의도적으로 `_admin_base` 를 상속하지
않습니다 — 로그인 전이거나 정상 화면 렌더링 자체가 불가능한 상황이라 사이드바·헤더 같은
"이미 로그인된 관리자" 전제의 UI 를 보여줄 수 없기 때문입니다.
<!-- @intent END -->
## 요구 사항
<!-- @generated:requirements START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| G7 코어 | `>=7.0.10` |
| PHP | `^8.2` |
<!-- @generated:requirements END -->
## 설치
<!-- @generated:install START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
```bash
# 번들 설치 (코어에 동봉된 소스에서 설치)
php artisan template:install sirsoft-admin_basic
# 활성화
php artisan template:activate sirsoft-admin_basic
# 업데이트 (번들 소스 기준 강제 반영)
php artisan template:update sirsoft-admin_basic --force
```
저장소: https://github.com/gnuboard/g7-template-sirsoft-admin_basic
<!-- @generated:install END -->
## 제공 컴포넌트
<!-- @generated:settings-summary START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
컴포넌트 125개 (루트: `src/components`).
| 분류 | 개수 |
|---|---|
| `basic` | 39개 |
| `composite` | 80개 |
| `layout` | 5개 |
| `modals` | 1개 |
<!-- @generated:settings-summary END -->
<!-- @intent START -->
운영자가 직접 켜고 끄는 설정 항목은 없습니다 — 이 표는 확장(모듈/플러그인) 개발자가 레이아웃을
만들 때 참고하는 컴포넌트 인벤토리입니다. 전체 목록·Props 상세는
[docs/components.md](docs/components.md) 와 코어
[component-props.md](../../../docs/frontend/component-props.md) 를 참고하세요.
<!-- @intent END -->
## 사용 방법
<!-- @intent START -->
**새 확장의 관리자 화면 만들기**: 확장의 `resources/layouts/admin/*.json` 에서
`"extends": "_admin_base"` 로 베이스를 상속하고, `slots.content` 에 필수 컴포넌트만으로 화면을
구성합니다. 필수 컴포넌트 목록 밖의 컴포넌트를 쓰면 다른 admin 템플릿으로 교체됐을 때 그
화면만 깨집니다.
**사이드바 메뉴 등록**: 확장이 `getAdminMenus()` 로 메뉴를 선언하면 이 템플릿의 `AdminSidebar`
가 자동으로 계층에 반영합니다 — 이 템플릿을 직접 수정할 필요가 없습니다.
**레이아웃 실시간 편집**: `/admin/templates/sirsoft-admin_basic/edit` 에서 레이아웃 편집기로
관리자 화면 자체를 코드 편집 + 실시간 미리보기로 수정할 수 있습니다(운영 환경에서는 신중하게
사용).
<!-- @intent END -->
## 다른 확장과의 연동
<!-- @generated:integrations START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
**이 확장이 의존하는 확장**
없음 — 코어만으로 동작합니다.
**이 확장에 의존하는 확장** (이 확장을 비활성화하면 함께 영향을 받습니다)
없음.
<!-- @generated:integrations END -->
<!-- @intent START -->
formal 의존성이 양쪽 다 "없음"인 것은 이 템플릿이 코어만으로 동작하기 때문이지만, 실질적으로는
**모든 번들 모듈/플러그인의 관리자 화면**이 이 템플릿의 필수 컴포넌트·`_admin_base` 계약에
암묵적으로 의존합니다. 이 의존은 manifest 로 선언되지 않습니다 — 어느 admin 템플릿이든 같은
계약(필수 컴포넌트 35개)만 구현하면 되므로, 확장이 "이 템플릿"이 아니라 "이 계약"에 의존하는
형태이기 때문입니다. 이 템플릿의 필수 컴포넌트 Props 를 바꿀 때 영향 범위를 이 템플릿
자신의 `dependencies` 목록으로는 알 수 없다는 뜻이며, 실제로는 활성 모듈/플러그인 전수의
관리자 레이아웃을 확인해야 합니다.
<!-- @intent END -->
## 문서
<!-- @generated:docs-index START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 문서 | 내용 | 상태 |
|---|---|---|
| [docs/README.md](docs/README.md) | 문서 통합 목차와 실측 집계 | ✅ |
| [docs/architecture.md](docs/architecture.md) | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
| [docs/components.md](docs/components.md) | 템플릿이 제공하는 컴포넌트 | ✅ |
| [docs/layouts.md](docs/layouts.md) | 레이아웃 목록과 라우트 매핑 | ✅ |
| [docs/handlers.md](docs/handlers.md) | 템플릿 전용 핸들러와 부트스트랩 | ✅ |
| [CHANGELOG.md](CHANGELOG.md) | 변경 이력 | ✅ |
<!-- @generated:docs-index END -->
## 트러블슈팅
<!-- @intent START -->
| 증상 | 원인 | 조치 |
|---|---|---|
| 사이드바 메뉴 아이콘이 안 보임 | `Icon` 컴포넌트로 렌더 시도(API 는 FontAwesome 클래스 문자열을 내려줌) | `I` 컴포넌트로 교체 (`docs/components.md` "AdminSidebar 상세" 참고) |
| 다른 admin 템플릿으로 교체 후 특정 확장 화면이 깨짐 | 그 확장이 필수 컴포넌트 목록 밖의 컴포넌트를 사용 | 그 확장의 레이아웃을 필수 컴포넌트(35개)만으로 재작성하거나, 새 템플릿에 같은 컴포넌트를 구현 |
| 사이드바 접힘 상태가 새로고침 후 풀림 | localStorage 접근 실패(시크릿 모드 등) 또는 부트스트랩 순서 문제 | `initSidebar()` 가 템플릿 부트스트랩에서 호출되는지 확인 (`src/index.ts`) |
| 로그인 화면에 다크 모드/언어 전환이 적용 안 됨 | 로그인 화면은 `_admin_base` 를 상속하지 않아 초기화 경로가 다름 | `admin_login.json` 자체의 초기화 액션을 확인 — `_admin_base` 수정으로는 반영되지 않는다 |
<!-- @intent END -->
## 변경 이력
[CHANGELOG.md](CHANGELOG.md)
## 라이선스
MIT
@@ -0,0 +1,20 @@
# Admin Basic 개발자 문서
> templates/_bundled/sirsoft-admin_basic · 템플릿
<!-- @generated:stats START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
**훅 수**: 0 · **구독 훅 수**: 0 · **라우트 수**: 29 · **모델 수**: 0 · **테이블 수**: 0 · **마이그레이션 수**: 0 · **레이아웃 수**: 145 · **핸들러 수**: 17
<!-- @generated:stats END -->
## 문서 목차
<!-- @generated:doc-toc START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 문서 | 내용 |
|---|---|
| [architecture.md](architecture.md) | 설계 의도·계층 지도·디렉토리 맵 |
| [components.md](components.md) | 템플릿이 제공하는 컴포넌트 |
| [layouts.md](layouts.md) | 레이아웃 목록과 라우트 매핑 |
| [handlers.md](handlers.md) | 템플릿 전용 핸들러와 부트스트랩 |
| [../AGENTS.md](../AGENTS.md) | 에이전트·확장개발자 진입점 |
| [../README.md](../README.md) | 사람(도입검토자·운영자) 진입점 |
<!-- @generated:doc-toc END -->
@@ -0,0 +1,60 @@
# Admin Basic — 아키텍처
> 설계 의도와 계층 구조 · 진입점: [AGENTS.md](../AGENTS.md)
## 설계 의도
<!-- @intent START -->
이 템플릿의 설계는 "관리자 화면 디자인"과 "관리자 화면 구성"을 분리하는 데 집중합니다.
컴포넌트(디자인 구현)는 이 템플릿이 소유하지만, 어떤 화면을 어떻게 구성할지(레이아웃 JSON)는
코어와 모든 번들 확장이 **각자 소유**합니다 — 이 템플릿은 그 구성을 그릴 부품만 제공합니다.
그래서 이 템플릿의 실질 소스는 컴포넌트(`src/components/`)와 코어 화면 레이아웃뿐이고, 확장
관리자 화면(모듈/플러그인이 소유)은 이 템플릿 디렉토리 밖(`modules/`, `plugins/`)에 있습니다.
필수 컴포넌트 계약(§AGENTS.md)이 이 분리를 지탱합니다 — 계약이 없으면 확장 개발자가 이
템플릿에만 있는 컴포넌트를 무심코 써버려 "관리자 화면 디자인 교체"가 사실상 불가능해집니다.
<!-- @intent END -->
## 계층 지도
<!-- @intent START -->
```
routes.json (URL → 레이아웃 이름 매핑)
│
▼
layouts/*.json (extends: _admin_base)
│
▼
_admin_base.json (사이드바·헤더·Toast·PageTransitionIndicator·콘텐츠 슬롯·푸터)
│
▼
src/components/{basic,composite,layout}/*.tsx (필수 컴포넌트 35개 포함 125개)
│
▼
src/handlers/*.ts (이 템플릿 전용 핸들러 17개 — 범용 핸들러는 코어 ActionDispatcher)
```
이 트리는 코어 화면에만 적용되는 것이 아닙니다 — 모듈/플러그인의 관리자 레이아웃도 같은
`_admin_base` → 컴포넌트 경로를 그대로 타므로, 이 템플릿을 고치면 그 확장들의 화면도 같은
방향으로 바뀝니다(§AGENTS.md "수정 시 동반 의무" 참고).
<!-- @intent END -->
## 디렉토리
<!-- @generated:directory-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 경로 | 역할 | 수정 시 필요한 절차 |
|---|---|---|
| `template.json` | manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json 동기화 |
| `routes.json` | 라우트 → 레이아웃 매핑 | `php artisan template:update sirsoft-admin_basic --force` |
| `layouts/` | 레이아웃 JSON | `php artisan template:update sirsoft-admin_basic --force` (빌드 불필요) |
| `src/components/` | React 컴포넌트 | `php artisan template:build` → `php artisan template:update sirsoft-admin_basic --force` |
| `src/handlers/` | 템플릿 전용 액션 핸들러 | `php artisan template:build` → `php artisan template:update sirsoft-admin_basic --force` |
| `dist/` | 커밋되는 빌드 산출물 | `--production` 으로 재빌드 (sourceMappingURL 잔존 금지) |
| `editor-spec.json` | 레이아웃 편집기 스펙 | `php artisan template:update sirsoft-admin_basic --force` |
| `editor-spec/` | 분할 편집기 스펙 | `php artisan template:update sirsoft-admin_basic --force` |
| `tests/` | 테스트 | 변경 범위만 필터 실행 |
| `CHANGELOG.md` | 변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
| `components.json` | 편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | `php artisan template:update sirsoft-admin_basic --force` |
| `docs/` | 개발자 문서 | 표면 변경 시 `php artisan ext:docgen` 재실행 |
| `lang/` | 다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
<!-- @generated:directory-map END -->
@@ -0,0 +1,587 @@
# Admin Basic — 컴포넌트
> 템플릿이 제공하는 컴포넌트 · 진입점: [AGENTS.md](../AGENTS.md)
## 제공 컴포넌트
<!-- @generated:components START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
컴포넌트 125개 (루트: `src/components`).
| 분류 | 개수 |
|---|---|
| `basic` | 39개 |
| `composite` | 80개 |
| `layout` | 5개 |
| `modals` | 1개 |
<!-- @generated:components END -->
<!-- @intent START -->
세 분류(basic/composite/layout)는 "얼마나 많이 조합됐는가"로 나뉩니다 — basic 은 HTML 태그를
그대로 래핑(`Div`→`<div>`)한 최소 단위, composite 는 basic 을 여러 개 조합해 UI 패턴을 캡슐화한
것(`DataGrid`가 `Table`+`Pagination`+정렬 로직을 감싸는 식), layout 은 페이지 구조 자체(Grid/
Flex/Container)를 정의합니다. 레이아웃 JSON 저작자는 이 구분을 몰라도 되지만("HTML 태그
직접 사용 금지" 규정만 지키면 됨), 컴포넌트를 새로 추가하는 쪽은 이 구분에 맞는 디렉토리
(`src/components/{basic,composite,layout}/`)에 넣어야 `components.json` 카탈로그와
`editor-spec.json` 팔레트 분류가 어긋나지 않습니다.
위 개수는 코드에서 실측되므로 시간이 지나면 달라집니다 — 이 문서에 구체적인 개수를 하드코딩해
적지 않습니다(과거 버전 문서가 37/66/8 로 적었다가 실제로는 39/80/5+modals 1 이 되어 있었던
사례가 있습니다). 정확한 전체 목록·Props 는 [component-props.md](../../../../docs/frontend/component-props.md)
· [component-props-composite.md](../../../../docs/frontend/component-props-composite.md) 를
따라갑니다 — 이 문서는 "이 템플릿에서만" 유효한 계약(필수 컴포넌트·AdminSidebar·SlotContainer)
만 다룹니다.
<!-- @intent END -->
## 필수 컴포넌트 (모듈 호환성)
모듈 개발자가 아래 컴포넌트만 사용하면 **다른 Admin 템플릿으로 교체해도 화면이 깨지지
않습니다**. 목록의 SSoT 는 `config/template.php` 의 `required_admin_components` 이며, 아래
표는 그 값을 그대로 옮긴 것입니다(코드가 SSoT — 표와 설정이 어긋나면 설정이 맞습니다).
| 분류 | 컴포넌트 |
|---|---|
| Basic (27개) | `A`, `Button`, `Checkbox`, `Div`, `Form`, `H1`, `H2`, `H3`, `Icon`, `Img`, `Input`, `Label`, `Li`, `Nav`, `P`, `Section`, `Select`, `Span`, `Svg`, `Table`, `Tbody`, `Td`, `Textarea`, `Th`, `Thead`, `Tr`, `Ul` |
| Composite (8개) | `Alert`, `Badge`, `Card`, `DataTable`, `FormField`, `Modal`, `PageHeader`, `Pagination` |
```text
✅ 필수 컴포넌트 목록에 있는 것만 사용 (다른 Admin 템플릿 호환 보장)
✅ 템플릿의 베이스 레이아웃(`_admin_base`)을 extends
❌ 이 템플릿에만 있는 커스텀 컴포넌트 사용 금지
```
기본은 `validate_on_install: false` — 설치 시 필수 컴포넌트 검증은 기본적으로 수행되지 않고
경고에 그칩니다(`block_on_failure: false`). 검증을 강제하려면 두 설정을 함께 켭니다.
## AdminSidebar 상세
계층형 관리자 메뉴를 그리는 컴포넌트로, 데이터 소스로 받은 메뉴 트리를 그대로 넘기면 됩니다.
```typescript
interface MenuItem {
id: string | number;
name: string | { ko: string; en: string };
slug: string;
url?: string | null;
icon?: string; // FontAwesome 클래스 (예: "fas fa-home")
children?: MenuItem[];
is_active?: boolean;
module_id?: number | null; // 모듈이 등록한 메뉴인지 식별
}
interface AdminSidebarProps {
logo?: string;
logoAlt?: string;
menu: MenuItem[]; // 필수
collapsed?: boolean;
onToggleCollapse?: () => void;
className?: string;
currentLocale?: string; // 미지정 시 G7Core.locale.current() 자동 사용
id?: string; // 레이아웃 편집기 코어 일괄 ID
}
```
아이콘은 `Icon` 컴포넌트(IconName enum)가 아니라 `I` 컴포넌트 + FontAwesome 클래스 문자열로
렌더링됩니다 — 메뉴 API 가 `icon` 필드를 이미 `"fas fa-home"` 형태 클래스 문자열로 내려주기
때문입니다. 새 메뉴 아이콘을 추가할 때 `Icon name="home"` 식으로 쓰면 렌더되지 않습니다.
## SlotContainer 상세
동적 슬롯 렌더링 컨테이너입니다 — 다른 컴포넌트가 `slot` prop 으로 자신이 속할 슬롯 ID 를
표현식으로 지정하면(예: `"{{_local.isVisible ? 'basic_filters' : 'detail_filters'}}"`), 그
표현식이 상태 변화에 따라 재평가되어 컴포넌트가 슬롯 사이를 동적으로 이동합니다.
```typescript
interface SlotContainerProps {
slotId: string; // 필수 — 렌더링할 슬롯 ID
className?: string;
}
```
```json
{ "id": "category_filter", "type": "basic", "name": "Div", "slot": "{{_local.isVisible ? 'basic_filters' : 'detail_filters'}}", "slotOrder": 1, "children": [] }
```
```json
{ "id": "basic_filters_container", "type": "composite", "name": "SlotContainer", "props": { "slotId": "basic_filters" } }
```
## 이관 원문 상세
> 아래는 코어 `docs/frontend/templates/sirsoft-admin_basic/components.md` 에 있던 원문을
> 이 문서로 옮긴 것입니다(#601). 이관 시점 그대로 보존하되, **코드가 SSoT 인 값과 어긋나는
> 부분에는 정정 주석**을 달았습니다 — 실측 총계는 위 「제공 컴포넌트」 블록이, 필수 컴포넌트
> 목록은 `config/template.php` 의 `required_admin_components` 가 SSoT 입니다.
### 컴포넌트 개요
> **정정(#601)**: 아래 개수는 이관 시점(2026-03) 문서 값입니다. 코드 실측은 basic 39 · composite 80 ·
> layout 5 · modals 1 = **125종**이며 위 「제공 컴포넌트」 블록이 SSoT 입니다. 아래 목록에 없는
> 컴포넌트가 있을 수 있습니다.
| 타입 | 개수 | 설명 |
|------|------|------|
| Basic | 37 | HTML 태그 래핑 — 최소 단위 컴포넌트 |
| Composite | 66 | 기본 컴포넌트 조합 — UI 패턴 캡슐화 |
| Layout | 8 | 페이지 구조 정의 — 컨테이너/그리드/플렉스 |
| **합계** | **111** | |
**소스**: `templates/_bundled/sirsoft-admin_basic/components.json`
**컴포넌트 소스**: `templates/_bundled/sirsoft-admin_basic/src/components/{basic,composite,layout}/*.tsx`
---
### Basic Components (37개)
> **정정(#601)**: 제목의 개수는 이관 시점 문서 값입니다(코드 실측 39종). 목록 자체는 원문 그대로입니다.
HTML 태그를 래핑하는 최소 단위 컴포넌트입니다. 모든 Basic 컴포넌트는 `className`, `children` props를 공통으로 지원합니다.
#### 텍스트/링크
| 컴포넌트 | 설명 | 주요 Props | 바인딩 |
|----------|------|-----------|--------|
| `A` | HTML anchor element wrapper | href, target | - |
| `H1` | HTML h1 heading element wrapper | - | - |
| `H2` | HTML h2 heading element wrapper | - | - |
| `H3` | HTML h3 heading element wrapper | - | - |
| `H4` | HTML h4 heading element wrapper | - | - |
| `P` | HTML paragraph element wrapper | - | - |
| `Span` | HTML span element wrapper | - | - |
| `Label` | HTML label element wrapper | - | - |
| `Pre` | 서식 유지 텍스트 래퍼 | - | - |
| `Code` | 인라인 코드 래퍼 | - | - |
#### 컨테이너
| 컴포넌트 | 설명 | 주요 Props | 바인딩 |
|----------|------|-----------|--------|
| `Div` | 범용 컨테이너 | - | - |
| `Section` | 시맨틱 섹션 | - | - |
| `Nav` | 네비게이션 래퍼 | - | - |
| `Form` | 폼 컨테이너 (자동 바인딩 지원) | - | - |
| `Fragment` | React.Fragment — iterator에서 DOM 래퍼 없이 사용 | - | - |
#### 폼 입력
| 컴포넌트 | 설명 | 주요 Props | 바인딩 |
|----------|------|-----------|--------|
| `Input` | 텍스트 입력 | label, error | checkable |
| `Select` | 선택 박스 | label, error | - |
| `Option` | Select 내부 옵션 | value | - |
| `Optgroup` | Select 옵션 그룹화 | label | - |
| `Textarea` | 텍스트 영역 | label, error | - |
| `Checkbox` | 체크박스 | label | checked |
| `FileInput` | 파일 입력 (검증 포함) | accept, maxSize, onChange, onError, buttonText, placeholder, disabled | - |
| `Button` | 버튼 | variant (`primary`\|`secondary`\|`danger`\|`success`), size (`sm`\|`md`\|`lg`) | - |
#### 테이블
| 컴포넌트 | 설명 |
|----------|------|
| `Table` | HTML table wrapper |
| `Thead` | 테이블 헤더 그룹 |
| `Tbody` | 테이블 본문 그룹 |
| `Tfoot` | 테이블 푸터 그룹 |
| `Tr` | 테이블 행 |
| `Th` | 테이블 헤더 셀 |
| `Td` | 테이블 데이터 셀 |
#### 리스트
| 컴포넌트 | 설명 |
|----------|------|
| `Ul` | 순서 없는 리스트 |
| `Ol` | 순서 있는 리스트 |
| `Li` | 리스트 아이템 |
#### 미디어/아이콘
| 컴포넌트 | 설명 | 주요 Props |
|----------|------|-----------|
| `Img` | 이미지 | src, alt |
| `Icon` | FontAwesome 아이콘 | name, iconStyle, size, color, spin, pulse, fixedWidth, ariaLabel |
| `Svg` | SVG 컨테이너 | - |
| `I` | HTML i 태그 (FontAwesome 클래스 직접 사용) | style |
---
### Composite Components (66개)
> **정정(#601)**: 제목의 개수는 이관 시점 문서 값입니다(코드 실측 80종). 목록 자체는 원문 그대로입니다.
기본 컴포넌트를 조합하여 UI 패턴을 캡슐화한 복합 컴포넌트입니다.
#### 데이터 표시
| 컴포넌트 | 설명 | 주요 Props |
|----------|------|-----------|
| `DataGrid` | 정렬/필터링/페이지네이션 데이터 그리드 | columns, data, sortable, pagination, pageSize, ... |
| `CardGrid` | 카드 그리드 레이아웃 (스켈레톤 로딩, 페이지네이션) | data, cardColumns, columns, gap, responsiveColumns, ... |
| `Pagination` | 페이지네이션 | currentPage, totalPages, onPageChange, maxVisiblePages, showFirstLast, ... |
| `Badge` | 색상 기반 라벨 뱃지 | color, text, size, style |
| `StatusBadge` | 상태 뱃지 (아이콘 포함) | status, label, showIcon, iconName, style |
| `StatCard` | 통계 카드 (값, 라벨, 추이 표시) | value, label, change, changeLabel, iconName, ... |
| `EmptyState` | 데이터 없음 상태 표시 | title, description, iconName, illustrationSrc, ... |
| `HtmlContent` | HTML 안전 렌더링 (DOMPurify XSS 방지) | content, isHtml, purifyConfig, text |
#### 폼/입력
| 컴포넌트 | 설명 | 주요 Props |
|----------|------|-----------|
| `FormField` | 폼 필드 래퍼 (라벨, 에러, 헬퍼 텍스트) | label, required, error, helperText, labelClassName, ... |
| `Toggle` | 토글 스위치 (Flowbite 스타일) | checked, value, onChange, disabled, label, ... |
| `TagInput` | 태그 입력 (다중 선택, 생성 가능) | value, options, onChange, creatable, placeholder, ... |
| `TagSelect` | 태그 기반 선택 표시 | options, value, onChange, placeholder, disabled |
| `RadioGroup` | 라디오 버튼 그룹 | name, value, options, onChange, disabled, ... |
| `SearchBar` | 검색 바 (자동완성 제안) | placeholder, value, onChange, onSearch, suggestions, ... |
| `SearchableDropdown` | 검색 가능 드롭다운 (단일/다중) | options, value, onChange, multiple, searchPlaceholder, ... |
| `ChipCheckbox` | 칩 스타일 체크박스 (필터 UI) | value, checked, icon, label, style, ... |
| `MultilingualInput` | 탭 방식 다국어 텍스트 입력 | value, onChange, inputType, availableLocales, defaultLocale, ... |
| `MultilingualTagInput` | 다국어 태그 입력 (모달 편집) | value, onChange, placeholder, disabled, creatable, ... |
| `MultilingualTabPanel` | 다국어 탭 패널 (로케일 제어) | style, variant, defaultLocale, onLocaleChange |
| `DynamicFieldList` | 동적 필드 목록 (드래그 정렬, 추가/삭제) | items, columns, onChange, onAddItem, onRemoveItem, ... |
| `FileUploader` | 파일 업로드 (드래그앤드롭, 이미지 압축) | attachmentableType, attachmentableId, collection, maxFiles, maxSize, ... |
| `IconSelect` | 아이콘 선택 드롭다운 | value, onChange, options, placeholder, searchPlaceholder, ... |
#### 에디터
| 컴포넌트 | 설명 | 주요 Props |
|----------|------|-----------|
| `HtmlEditor` | HTML/텍스트 편집기 (편집/미리보기 토글) | content, onChange, isHtml, onHtmlModeChange, previewMode, ... |
| `CodeEditor` | JSON 코드 편집기 (Monaco Editor) | value, onChange, language, height, readOnly, ... |
#### 네비게이션
| 컴포넌트 | 설명 | 주요 Props |
|----------|------|-----------|
| `TabNavigation` | 탭 네비게이션 | tabs, activeTabId, onTabChange, variant, style |
| `TabNavigationScroll` | 탭 네비게이션 + 스크롤 (setState + scrollToSection 내장) | tabs, activeTabId, actions, style, activeClassName, ... |
| `Breadcrumb` | 브레드크럼 | items, separator, showHome, homeHref, maxItems |
| `ActionMenu` | 드롭다운 액션 메뉴 | items, triggerLabel, triggerIconName, position, style |
| `Dropdown` | 드롭다운 메뉴 | label, items, onItemClick, position, style |
#### 모달/다이얼로그
| 컴포넌트 | 설명 | 주요 Props |
|----------|------|-----------|
| `Modal` | 모달 다이얼로그 (오버레이, 포커스 트랩) | isOpen, onClose, title, width, style |
| `Dialog` | 다이얼로그 (Modal 별칭) | isOpen, onClose, title, content, actions, ... |
| `ConfirmDialog` | 확인/취소 다이얼로그 | isOpen, onClose, title, message, confirmText, ... |
| `AlertDialog` | 알림 다이얼로그 (확인 버튼만) | isOpen, onClose, title, message, confirmText, ... |
| `Toast` | 토스트 알림 | toasts, position, onRemove |
| `Alert` | 알림 메시지 | type, message, dismissible, onDismiss |
#### 관리자 UI
| 컴포넌트 | 설명 | 주요 Props |
|----------|------|-----------|
| `AdminSidebar` | 관리자 사이드바 (계층형 메뉴) | logo, logoAlt, menu, collapsed, onToggleCollapse |
| `AdminHeader` | 관리자 헤더 | user, notifications, onNotificationClick, onProfileClick, onLogoutClick |
| `AdminFooter` | 관리자 푸터 | copyright, version, quickLinks |
| `PageHeader` | 페이지 헤더 (제목, 브레드크럼, 액션) | title, subtitle, breadcrumbItems, tabs, onTabChange, ... |
| `LoginForm` | 로그인 폼 (이메일/비밀번호 검증) | submitButtonText, emailPlaceholder, passwordPlaceholder, forgotPasswordText, forgotPasswordUrl, ... |
| `UserProfile` | 사용자 프로필 드롭다운 | user, profileText, logoutText, onProfileClick, onLogoutClick |
| `NotificationCenter` | 알림 센터 | notifications, titleText, emptyText, onNotificationClick |
| `ThemeToggle` | 테마 모드 전환 (다크/라이트/자동) | onThemeChange, autoText, lightText, darkText |
| `LanguageSelector` | 언어 선택 드롭다운 | availableLocales, languageText, apiEndpoint, onLanguageChange, inline |
| `PageTransitionIndicator` | 페이지 전환 로딩 표시 | style |
#### 레이아웃 편집
| 컴포넌트 | 설명 | 주요 Props |
|----------|------|-----------|
| `LayoutEditorHeader` | 레이아웃 편집기 헤더 | layoutName, onBack, onPreview, onSave, isSaving |
| `LayoutFileList` | 레이아웃 파일 목록 | files, selectedId, onSelect |
| `LayoutHistoryPanel` | 레이아웃 히스토리 패널 | layoutId, versions, onRestore |
| `LayoutWarnings` | 레이아웃 경고 표시 | warnings |
| `VersionList` | 버전 목록 아이템 | versions, selectedId, onSelect |
#### 확장 관리
| 컴포넌트 | 설명 | 주요 Props |
|----------|------|-----------|
| `TemplateCard` | 템플릿 카드 (설치/활성화) | image, imageAlt, vendor, name, version, ... |
| `ExtensionBadge` | 확장 섹션 뱃지 (identifier로 이름 자동 조회) | type, identifier, name, installedModules, installedPlugins, ... |
| `ProductCard` | 상품 카드 (이미지, 제목, 가격, 액션) | imageUrl, imageAlt, title, subtitle, description, ... |
#### 고급 기능
| 컴포넌트 | 설명 | 주요 Props |
|----------|------|-----------|
| `SlotContainer` | 동적 슬롯 렌더링 컨테이너 | slotId, emptyContent, style, id |
| `FilterGroup` | 다중 필터 그룹 | title, filters, onChange, onReset, showResetButton, ... |
| `FilterVisibilitySelector` | 필터 가시성 상태 관리 (UI 없음, localStorage 저장) | id, visibleFilters, defaultFilters, onFilterVisibilityChange |
| `ColumnSelector` | 테이블 컬럼 표시/숨김 선택 드롭다운 | columns, visibleColumns, onColumnVisibilityChange, triggerLabel, triggerIconName, ... |
| `PermissionTree` | 계층형 권한 트리 (체크박스 선택) | data, value, onChange, disabled, desktopColumns |
| `CategoryTree` | 계층형 카테고리 트리 (체크박스 선택) | data, expandedIds, selectedIds, searchKeyword, showProductCount, ... |
| `SortableMenuList` | 드래그앤드롭 계층형 메뉴 목록 | items, selectedId, onSelect, onOrderChange, onToggleStatus, ... |
| `SortableMenuItem` | 개별 드래그 가능 메뉴 아이템 | item, isSelected, isExpanded, level, onClick, ... |
| `IconButton` | 아이콘 버튼 | iconName, label, onClick, variant, size, ... |
| `Accordion` | 아코디언 (접기/펼치기) | defaultOpen, isOpen, onToggle, style, disabled |
| `Card` | 카드 컨테이너 (헤더, 본문, 푸터) | title, content, imageUrl, imageAlt, onClick, ... |
| `LoadingSpinner` | 로딩 스피너 | size, color, fullscreen, text |
| `ImageGallery` | 라이트박스 이미지 갤러리 (줌, 다운로드, 썸네일) | images, initialIndex, onClose |
---
### Layout Components (8개)
> **정정(#601)**: 제목의 개수는 이관 시점 문서 값입니다(코드 실측 5종). 목록 자체는 원문 그대로입니다.
페이지 구조를 정의하는 레이아웃 컴포넌트입니다.
| 컴포넌트 | 설명 | 주요 Props |
|----------|------|-----------|
| `Container` | Flex/Grid 컨테이너 | mode, direction, justify, align, wrap, gap, cols, responsive, padding, maxWidth, centered, style |
| `Grid` | CSS Grid 반응형 그리드 | cols, responsive, gap, rowGap, colGap, autoRows, autoCols, flow, style |
| `Flex` | Flexbox 레이아웃 | direction, justify, align, wrap, gap, grow, shrink, style |
| `SectionLayout` | 섹션 레이아웃 (스타일 옵션) | title, subtitle, padding, background, maxWidth, centered, border, shadow, rounded, style |
| `ThreeColumnLayout` | 3열 레이아웃 (좌/중/우 슬롯) | leftWidth, rightWidth, leftSlot, centerSlot, rightSlot, style |
| `RichSelect` | 커스텀 항목 렌더링 셀렉트 | options, value, onChange, placeholder, disabled, maxHeight, selectedChildren |
| `DropdownButton` | 포탈 기반 드롭다운 버튼 | label, icon, iconPosition, position |
| `DropdownMenuItem` | DropdownButton 내부 메뉴 아이템 | label, icon, variant, disabled, divider |
---
### 필수 컴포넌트 (모듈 호환성)
모듈 개발자가 이 컴포넌트들만 사용하면 **모든 Admin 템플릿에서 동작이 보장**됩니다.
#### 필수 Basic (27개)
| 카테고리 | 컴포넌트 |
|----------|----------|
| 텍스트/링크 | `A`, `H1`, `H2`, `H3`, `P`, `Span`, `Label` |
| 컨테이너 | `Div`, `Section`, `Nav` |
| 폼 | `Form`, `Input`, `Select`, `Checkbox`, `Textarea`, `Button` |
| 테이블 | `Table`, `Thead`, `Tbody`, `Tr`, `Th`, `Td` |
| 리스트 | `Ul`, `Li` |
| 미디어 | `Icon`, `Img`, `Svg` |
#### 필수 Composite (15개)
> **정정(#601)**: 아래 표는 `config/template.php` 의 `required_admin_components` 와 일치하지 않습니다 —
> 실제 필수 Composite 는 위 「필수 컴포넌트 (모듈 호환성)」 절의 8종(`Alert` `Badge` `Card`
> `DataTable` `FormField` `Modal` `PageHeader` `Pagination`)이며 설정 파일이 SSoT 입니다.
> 아래 목록은 이관 시점 문서를 보존한 것이므로 필수 여부의 근거로 쓰지 않습니다.
| 카테고리 | 컴포넌트 | 설명 |
|----------|----------|------|
| 데이터 표시 | `DataGrid` | 목록 페이지 필수 (테이블 형식) |
| | `CardGrid` | 목록 페이지 필수 (카드 형식) |
| | `Pagination` | 페이지네이션 |
| | `Badge` | 상태 표시 |
| 폼 | `FormField` | 폼 필드 래퍼 |
| | `Toggle` | 토글 스위치 |
| | `TagInput` | 태그 입력 |
| 에디터 | `HtmlEditor` | HTML/텍스트 에디터 |
| | `CodeEditor` | 코드 에디터 (Monaco) |
| 피드백 | `Modal` | 모달 다이얼로그 |
| | `Alert` | 알림 메시지 |
| 레이아웃 | `PageHeader` | 페이지 헤더 |
| | `Card` | 카드 컨테이너 |
| | `AdminSidebar` | 관리자 사이드바 |
| | `SlotContainer` | 동적 슬롯 렌더링 |
#### 설정 파일
> **정정(#601)**: 아래 PHP 스니펫은 이관 시점 값입니다. 현재 `config/template.php` 의 실제 배열은 위
> 「필수 컴포넌트 (모듈 호환성)」 절의 표(Basic 27 + Composite 8)와 일치합니다.
필수 컴포넌트 목록은 `config/template.php`에 정의:
```php
'required_admin_components' => [
'DataTable', 'Pagination', 'Badge',
'Form', 'FormField', 'Input', 'Select', 'Checkbox',
'Button', 'Modal', 'Alert',
'PageHeader', 'Card',
],
```
---
### 모듈 개발자 가이드
#### 핵심 원칙
```text
✅ 필수 컴포넌트 목록에 있는 컴포넌트만 사용
✅ 템플릿의 베이스 레이아웃 (_admin_base)을 extends
❌ 특정 템플릿에만 존재하는 커스텀 컴포넌트 사용 금지
```
#### 레이아웃 작성 예시
```json
{
"version": "1.0.0",
"layout_name": "sirsoft-ecommerce_admin_products_index",
"extends": "_admin_base",
"slots": {
"content": [
{
"id": "products-table",
"type": "composite",
"name": "DataGrid",
"props": {
"columns": [],
"data": "{{products?.data?.data}}"
}
}
]
}
}
```
---
### 템플릿 개발자 가이드
Admin 타입 템플릿 개발 시 필수 컴포넌트를 반드시 구현해야 합니다.
#### 구현 체크리스트
```text
□ DataGrid - 정렬, 필터링, 페이지네이션 지원
□ Pagination - 페이지 이동, 페이지 크기 변경
□ Badge - 다양한 상태 색상 지원
□ Form - 유효성 검사, 제출 처리
□ FormField - 라벨, 에러 메시지, 필수 표시
□ Input - 텍스트, 이메일, 비밀번호 등 타입 지원
□ Select - 단일/다중 선택, 검색 기능
□ Checkbox - 단일/그룹 체크박스
□ Button - 다양한 variant와 size
□ Modal - 열기/닫기, 확인/취소 액션
□ Alert - success, warning, error, info 타입
□ PageHeader - 제목, 브레드크럼, 액션 버튼
□ Card - 헤더, 본문, 푸터 영역
□ AdminSidebar - 계층형 메뉴, 다국어 지원
```
#### Props 인터페이스 일관성
모든 Admin 템플릿은 동일한 Props 인터페이스를 구현해야 합니다. 상세 Props는 다음 문서를 참조:
- [컴포넌트 Props 레퍼런스 (Basic)](../../../../docs/frontend/component-props.md)
- [컴포넌트 Props 레퍼런스 (Composite)](../../../../docs/frontend/component-props-composite.md)
---
### AdminSidebar 상세
#### MenuItem 인터페이스
```typescript
interface MenuItem {
id: string | number;
name: string | { ko: string; en: string };
slug: string;
url?: string | null;
icon?: string; // FontAwesome 클래스 (예: "fas fa-home")
children?: MenuItem[];
is_active?: boolean;
}
```
#### AdminSidebarProps
```typescript
interface AdminSidebarProps {
logo?: string;
logoAlt?: string;
menu: MenuItem[]; // 필수
collapsed?: boolean;
onToggleCollapse?: () => void;
className?: string;
currentLocale?: string; // 기본값: 'ko'
}
```
#### 아이콘 처리
```text
✅ I 컴포넌트 + FontAwesome 클래스 (<I className="fas fa-home w-5 h-5" />)
❌ Icon 컴포넌트 + IconName enum (금지 — API가 FontAwesome 클래스 문자열 직접 제공)
```
#### 레이아웃 JSON 사용 예시
```json
{
"data_sources": [
{
"id": "admin_menu",
"type": "api",
"endpoint": "/api/admin/menus",
"method": "GET",
"auto_fetch": true,
"auth_required": true
}
],
"components": [
{
"id": "admin_sidebar",
"type": "composite",
"name": "AdminSidebar",
"props": {
"menu": "{{admin_menu.data}}"
}
}
]
}
```
---
### SlotContainer 상세
#### SlotContainerProps
```typescript
interface SlotContainerProps {
slotId: string; // 필수 — 렌더링할 슬롯 ID
className?: string;
}
```
#### 슬롯 시스템 동작
```text
1. slot 속성 컴포넌트 → SlotContext에 등록
2. SlotContainer가 해당 slotId의 컴포넌트 렌더링
3. 상태 변화 시 slot 표현식 재평가로 동적 이동
```
#### 사용 예시
```json
{
"id": "category_filter",
"type": "basic",
"name": "Div",
"slot": "{{_local.isVisible ? 'basic_filters' : 'detail_filters'}}",
"slotOrder": 1,
"children": []
}
```
```json
{
"id": "basic_filters_container",
"type": "composite",
"name": "SlotContainer",
"props": { "slotId": "basic_filters" }
}
```
---
### 관련 문서
- [sirsoft-admin_basic 핸들러](handlers.md)
- [sirsoft-admin_basic 레이아웃](layouts.md)
- [sirsoft-basic 컴포넌트](../../../../docs/frontend/templates/sirsoft-basic/components.md)
- [컴포넌트 개발 규칙](../../../../docs/frontend/components.md)
- [컴포넌트 Props 레퍼런스](../../../../docs/frontend/component-props.md)
- [컴포넌트 Props 레퍼런스 - Composite](../../../../docs/frontend/component-props-composite.md)
@@ -0,0 +1,584 @@
# Admin Basic — 핸들러
> 템플릿 전용 핸들러와 부트스트랩 · 진입점: [AGENTS.md](../AGENTS.md)
## 템플릿 전용 핸들러
<!-- @generated:handlers START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
핸들러 17개 (정의: `src/handlers/index.ts`).
| 핸들러 | 레이아웃에서 부르는 이름 |
|---|---|
| `detectAssetUrlMode` | (템플릿 전용 — 네임스페이스 없음) |
| `checkAssetUrlModeDrift` | (템플릿 전용 — 네임스페이스 없음) |
| `setTheme` | (템플릿 전용 — 네임스페이스 없음) |
| `initTheme` | (템플릿 전용 — 네임스페이스 없음) |
| `scrollToSection` | (템플릿 전용 — 네임스페이스 없음) |
| `initMenuFromUrl` | (템플릿 전용 — 네임스페이스 없음) |
| `initFilterVisibility` | (템플릿 전용 — 네임스페이스 없음) |
| `saveFilterVisibility` | (템플릿 전용 — 네임스페이스 없음) |
| `toggleFilterVisibility` | (템플릿 전용 — 네임스페이스 없음) |
| `resetFilterVisibility` | (템플릿 전용 — 네임스페이스 없음) |
| `saveMultilingualTag` | (템플릿 전용 — 네임스페이스 없음) |
| `cancelMultilingualTag` | (템플릿 전용 — 네임스페이스 없음) |
| `updateMultilingualTagValue` | (템플릿 전용 — 네임스페이스 없음) |
| `setDateRange` | (템플릿 전용 — 네임스페이스 없음) |
| `toggleSidebar` | (템플릿 전용 — 네임스페이스 없음) |
| `initSidebar` | (템플릿 전용 — 네임스페이스 없음) |
| `downloadAttachment` | (템플릿 전용 — 네임스페이스 없음) |
<!-- @generated:handlers END -->
<!-- @intent START -->
`setLocale` 이 이 목록에 없는 것이 정상입니다 — 로케일 전환은 엔진 레벨(ActionDispatcher)
빌트인으로 승격되어 더 이상 이 템플릿이 등록하지 않습니다(`src/handlers/index.ts` 상단 주석
참고). 과거 버전 문서가 `setLocale` 을 이 템플릿의 핸들러로 적었다면 그것은 이관 전 버전
기준이므로 따르지 않습니다. 새 핸들러를 추가할 때는 `src/handlers/index.ts` 의 맵 객체 한
곳에만 등록하면 자동으로 ActionDispatcher 에 반영됩니다 — 다른 곳에 흩어 등록하지 않습니다.
이 핸들러들은 **이 템플릿에서만** 등록되므로 다른 템플릿에서는 미지원일 수 있습니다. 범용
핸들러(`navigate`/`apiCall`/`setState` 등)는 [actions-handlers.md](../../../../docs/frontend/actions-handlers.md)
를 참고하고, 여기서는 이 템플릿 전용 핸들러만 다룹니다.
### setTheme / initTheme
다크/라이트/자동(`auto`, 시스템 설정 따름) 테마를 전환·복원합니다. `setTheme` 은 localStorage
저장 + `document.documentElement` 클래스 적용(Tailwind `dark:` variant 활성화)을,
`initTheme` 은 params 없이 `init_actions` 에서 호출해 저장된 테마를 앱 시작 시 복원합니다.
```json
{ "type": "click", "handler": "setTheme", "params": { "theme": "{{_global.theme === 'dark' ? 'light' : 'dark'}}" } }
```
### scrollToSection
`params.selector`(CSS 선택자, 필수) 로 지정한 요소로 부드럽게 스크롤합니다. `params.offset`
(기본 `0`, 음수면 위로)은 고정 헤더 높이를 보상할 때 씁니다.
```json
{ "type": "click", "handler": "scrollToSection", "params": { "selector": "#features", "offset": -80 } }
```
### initMenuFromUrl
현재 URL 경로를 사이드바 메뉴 항목과 매칭해 활성 메뉴(및 부모 메뉴의 펼침 상태)를 자동
설정합니다. params 없이 `_admin_base.json` 의 `init_actions` 에서 호출합니다.
### 필터 가시성 핸들러 4종
목록 화면 필터 패널의 표시/숨김을 localStorage 에 저장해 새로고침 후에도 유지합니다.
| 핸들러 | params | 설명 |
|---|---|---|
| `initFilterVisibility` | 없음 | localStorage → `_local` 복원 (`init_actions`에서 호출) |
| `saveFilterVisibility` | `{ filters }` | `_local` → localStorage 저장 |
| `toggleFilterVisibility` | `{ key }` | 특정 필터 키 가시성 토글 |
| `resetFilterVisibility` | 없음 | 전체 초기화 |
### 다국어 태그 핸들러 3종
`MultilingualInput` 컴포넌트가 쓰는 태그 편집 핸들러입니다.
| 핸들러 | params | 설명 |
|---|---|---|
| `saveMultilingualTag` | `{ field, locale }` | 태그 저장 |
| `cancelMultilingualTag` | 없음 | 편집 취소 |
| `updateMultilingualTagValue` | `{ field, locale, value }` | 값 업데이트 |
### setDateRange
날짜 필터 프리셋 버튼(`today`/`week`/`month`/`3months`/`6months`/`1year`) 클릭 시 시작·종료일을
`YYYY-MM-DDTHH:mm:ss` 형식으로 계산해 **반환**합니다(자체적으로 상태를 갱신하지 않음) — 레이아웃
JSON 이 `sequence` + `setState` 로 반환값(`$prev.startDate` 등)을 원하는 필드에 반영합니다.
```json
{ "type": "click", "handler": "sequence", "actions": [
{ "handler": "setDateRange", "params": { "preset": "today" } },
{ "handler": "setState", "params": { "target": "local", "filter.dateFrom": "{{$prev.startDate}}", "filter.dateTo": "{{$prev.endDate}}" } }
] }
```
### 사이드바 접힘 핸들러 2종
데스크톱 좌측 사이드바 접힘 상태를 `localStorage`(`g7_admin_sidebar_collapsed`) 와
`_global.sidebarCollapsed` 에 반영합니다. 모바일 슬라이드 사이드바 상태(`_global.sidebarOpen`)와는
**독립된 상태**이므로 데스크톱 접힘과 모바일 열림/닫힘을 같은 상태로 착각하지 않습니다.
| 핸들러 | params | 설명 |
|---|---|---|
| `initSidebar` | 없음 | 저장된 접힘 상태 복원 (`init_actions`에서 호출) |
| `toggleSidebar` | 없음 | 접힘 상태 반전 + 저장 |
### downloadAttachment
관리자 화면에서 첨부파일을 `<a href>` 직접 링크가 아니라 `G7Core.api.get(url, {responseType:
'blob'})` 로 요청한 뒤 objectURL 로 변환해 다운로드합니다. `<a>` 네비게이션은 Authorization
헤더가 실리지 않아 요청이 guest 로 통과하고 활동이력의 행위자(`user_id`)가 비게 되므로, 코어
ApiClient 경유로 토큰을 자동 첨부해야 다운로드 행위가 관리자 본인 이력으로 정확히 남습니다.
```json
{ "type": "click", "handler": "downloadAttachment", "params": { "url": "{{attachment.download_url}}", "filename": "{{attachment.original_name}}" } }
```
### 자산 URL 방식 재감지 2종
관리자 환경설정 화면에서 정적 자산 URL 방식(확장자 있음/없음)을 **브라우저에서** 재감지합니다.
서버가 자기 자신에게 curl 하면 nginx vhost·프록시 체인을 우회해 오판할 수 있어, 실제 방문자
경로를 재현하려면 브라우저가 프로브를 던져야 합니다.
| 핸들러 | 호출 시점 | 동작 |
|---|---|---|
| `detectAssetUrlMode` | 관리자가 "재감지" 버튼 클릭 | 프로브 쌍(확장자 있음/없음)을 던져 판정한 뒤 폼 상태(`general.asset_url_mode`)와 감지 상태 문구를 갱신 — **저장은 하지 않음**(관리자가 결과를 보고 직접 저장) |
| `checkAssetUrlModeDrift` | 대시보드 진입 시 자동 | 저장된 설정값과 실제 감지 결과를 대조해 어긋나면 전역 상태(`assetUrlModeDrift`)에 실어 대시보드에 경고 노출 |
봇은 JavaScript 를 실행하지 않으므로 클라이언트측 자가 복구가 있어도 저장값이 틀린 채로
남으면 SEO 렌더링은 계속 깨집니다 — `checkAssetUrlModeDrift` 가 그 간극을 관리자에게 드러내는
유일한 지점입니다. 판정 불가(`unavailable`)일 때는 아무 것도 표시하지 않습니다 — 일시적
네트워크 장애를 결함 신호로 오인시키지 않기 위함입니다.
<!-- @intent END -->
## 부트스트랩
<!-- @generated:frontend-entry START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 항목 | 값 |
|---|---|
| 엔트리 파일 | `src/index.ts` |
| 전역 객체 | **미노출** |
| 재등록 진입점 | `initTemplate()` |
재등록 진입점이 전역에 고정 이름으로 노출되지 않으면 로케일 전환 후 이 확장의 액션이 전부 무반응이 됩니다 (오류·토스트 없음).
<!-- @generated:frontend-entry END -->
<!-- @intent START -->
전역 객체가 "미노출"인 것은 실수가 아니라 템플릿과 모듈/플러그인의 재등록 규약 차이입니다 —
모듈/플러그인은 여러 개가 동시에 활성화되므로 서로를 식별할 `window.__[Name]` 고정 이름이
필요하지만, 템플릿은 사이트에 활성 템플릿이 항상 하나뿐이라 그런 식별 필요가 없습니다.
`initTemplate()` 을 고칠 때는 핸들러 재등록
외의 1회성 부팅 작업(예: 사이드바 초기 상태 복원)을 섞지 않습니다 — 로케일 전환마다 재실행되면
안 되는 작업이기 때문입니다(사이드바 복원은 `initSidebar` 를 레이아웃 `init_actions` 에서
별도로 호출하는 이유이기도 합니다).
<!-- @intent END -->
## 이관 원문 상세
> 아래는 코어 `docs/frontend/templates/sirsoft-admin_basic/handlers.md` 에 있던 원문을
> 이 문서로 옮긴 것입니다(#601). 핸들러별 params·동작·사용 예시가 여기에 있습니다.
> 이관 시점 그대로 보존하되, 현재 코드와 어긋나는 부분에는 정정 주석을 달았습니다 —
> 등록 핸들러의 SSoT 는 위 「템플릿 전용 핸들러」 블록입니다.
### setLocale
> **정정(#601)**: `setLocale` 은 더 이상 이 템플릿이 등록하는 핸들러가 아닙니다 — 엔진(ActionDispatcher)
> 빌트인으로 승격되어 모든 템플릿에서 동작합니다. 아래 서술은 이관 시점 기록이며, 동작·파라미터는
> 같지만 **소유 주체가 템플릿이 아니라 엔진**입니다.
앱 언어를 변경합니다. 번역 파일을 다시 로드하고 UI를 갱신합니다.
**소스**: `src/handlers/setLocaleHandler.ts`
```json
{
"type": "click",
"handler": "setLocale",
"params": {
"locale": "en"
}
}
```
#### params
| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `locale` | string | ✅ | 변경할 로케일 코드 (예: `"ko"`, `"en"`, `"ja"`) |
#### 동작
```text
1. 로케일 변경 → 번역 파일 다시 로드
2. _global.locale 자동 업데이트
3. 모든 $t: 표현식 재평가
```
#### 사용 예시
```json
{
"id": "lang_en_btn",
"type": "basic",
"name": "Button",
"props": {
"text": "English"
},
"actions": [
{
"type": "click",
"handler": "setLocale",
"params": {
"locale": "en"
}
}
]
}
```
---
### setTheme / initTheme
#### setTheme
다크/라이트 모드를 전환합니다.
**소스**: `src/handlers/setThemeHandler.ts`
```json
{
"type": "click",
"handler": "setTheme",
"params": {
"theme": "dark"
}
}
```
#### setTheme params
| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `theme` | string | ✅ | `"light"`, `"dark"`, `"auto"` (시스템 설정 따름) |
#### 동작
```text
1. localStorage에 테마 설정 저장
2. document.documentElement에 class 적용 (dark/light)
3. Tailwind dark: variant 활성화/비활성화
```
#### initTheme
앱 시작 시 저장된 테마 설정을 적용합니다. 주로 `init_actions`에서 사용합니다.
```json
{
"init_actions": [
{
"handler": "initTheme"
}
]
}
```
params 없이 호출합니다. localStorage에 저장된 테마 설정을 복원합니다.
#### 사용 예시
```json
{
"id": "theme_toggle",
"type": "composite",
"name": "ThemeToggle",
"actions": [
{
"type": "click",
"handler": "setTheme",
"params": {
"theme": "{{_global.theme === 'dark' ? 'light' : 'dark'}}"
}
}
]
}
```
---
### scrollToSection
특정 섹션으로 스크롤합니다. 오프셋 지원에 특화되어 있습니다.
**소스**: `src/handlers/scrollToSectionHandler.ts`
```json
{
"type": "click",
"handler": "scrollToSection",
"params": {
"selector": "#features",
"offset": -80
}
}
```
#### params
| 필드 | 타입 | 필수 | 기본값 | 설명 |
|------|------|------|--------|------|
| `selector` | string | ✅ | - | CSS 선택자 (예: `"#section-id"`, `".class-name"`) |
| `offset` | number | ❌ | `0` | 스크롤 오프셋 (음수: 위로, 양수: 아래로). 고정 헤더 높이 보상에 사용 |
#### 동작
```text
1. document.querySelector(selector)로 대상 요소 검색
2. 요소의 위치 계산 + offset 적용
3. window.scrollTo({ top, behavior: 'smooth' })로 부드러운 스크롤
```
#### 사용 예시
```json
{
"id": "nav_features",
"type": "basic",
"name": "A",
"props": {
"text": "$t:common.features",
"className": "cursor-pointer"
},
"actions": [
{
"type": "click",
"handler": "scrollToSection",
"params": {
"selector": "#features",
"offset": -80
}
}
]
}
```
---
### initMenuFromUrl
현재 URL을 기반으로 사이드바/네비게이션 메뉴의 활성 상태를 초기화합니다. 주로 관리자 템플릿의 `init_actions`에서 사용합니다.
**소스**: `src/handlers/initMenuFromUrlHandler.ts`
```json
{
"init_actions": [
{
"handler": "initMenuFromUrl"
}
]
}
```
#### params
없음. 현재 URL 경로를 메뉴 항목과 매칭하여 활성 메뉴를 자동 설정합니다.
#### 동작
```text
1. 현재 URL 경로 (window.location.pathname) 추출
2. 사이드바 메뉴 데이터에서 URL 매칭
3. 매칭된 메뉴 항목의 is_active 상태 설정
4. 부모 메뉴도 자동으로 펼침 상태 설정
```
#### 사용 예시 (_admin_base.json)
```json
{
"init_actions": [
{ "handler": "initTheme" },
{ "handler": "initMenuFromUrl" }
]
}
```
---
### 필터 가시성 핸들러
목록 화면에서 필터 패널의 가시성(표시/숨김)을 관리합니다. localStorage에 상태를 저장하여 새로고침 후에도 유지합니다.
**소스**: `src/handlers/filterVisibilityHandler.ts`
#### initFilterVisibility
저장된 필터 가시성 상태를 `_local`에 복원합니다.
```json
{
"init_actions": [
{
"handler": "initFilterVisibility"
}
]
}
```
#### saveFilterVisibility
현재 필터 가시성 상태를 localStorage에 저장합니다.
```json
{
"handler": "saveFilterVisibility",
"params": {
"filters": "{{_local.filterVisibility}}"
}
}
```
#### toggleFilterVisibility
특정 필터 키의 가시성을 토글합니다.
```json
{
"type": "click",
"handler": "toggleFilterVisibility",
"params": {
"key": "advancedFilters"
}
}
```
#### resetFilterVisibility
모든 필터 가시성을 초기 상태로 리셋합니다.
```json
{
"type": "click",
"handler": "resetFilterVisibility"
}
```
#### 핸들러 params 요약
| 핸들러 | params | 설명 |
|--------|--------|------|
| `initFilterVisibility` | 없음 | localStorage → `_local` 복원 |
| `saveFilterVisibility` | `{ filters }` | `_local` → localStorage 저장 |
| `toggleFilterVisibility` | `{ key }` | 특정 키 토글 |
| `resetFilterVisibility` | 없음 | 전체 초기화 |
#### 사용 예시 (목록 페이지)
```json
{
"init_actions": [
{ "handler": "initFilterVisibility" }
],
"components": [
{
"id": "filter_toggle_btn",
"type": "basic",
"name": "Button",
"props": {
"text": "$t:common.toggle_filters"
},
"actions": [
{
"type": "click",
"handler": "toggleFilterVisibility",
"params": { "key": "advancedFilters" }
}
]
},
{
"id": "filter_section",
"type": "basic",
"name": "Div",
"if": "{{_local.filterVisibility?.advancedFilters}}",
"children": [
{ "comment": "필터 컴포넌트들" }
]
}
]
}
```
---
### 다국어 태그 핸들러
다국어 입력 컴포넌트(MultilingualInput)에서 사용하는 태그 관리 핸들러입니다.
**소스**: `src/handlers/multilingualTagHandler.ts`
#### saveMultilingualTag
다국어 태그를 저장합니다.
```json
{
"handler": "saveMultilingualTag",
"params": {
"field": "tags",
"locale": "{{_global.locale}}"
}
}
```
#### cancelMultilingualTag
다국어 태그 편집을 취소합니다.
```json
{
"handler": "cancelMultilingualTag"
}
```
#### updateMultilingualTagValue
다국어 태그 값을 업데이트합니다.
```json
{
"handler": "updateMultilingualTagValue",
"params": {
"field": "tags",
"locale": "ko",
"value": "{{$event.target.value}}"
}
}
```
#### 태그 핸들러 params 요약
| 핸들러 | params | 설명 |
|--------|--------|------|
| `saveMultilingualTag` | `{ field, locale }` | 태그 저장 |
| `cancelMultilingualTag` | 없음 | 편집 취소 |
| `updateMultilingualTagValue` | `{ field, locale, value }` | 값 업데이트 |
---
### 핸들러 소스 파일 매핑
| 핸들러명 | 소스 파일 | 등록 함수 |
|---------|----------|----------|
| `setLocale` | `src/handlers/setLocaleHandler.ts` | `setLocaleHandler` |
| `setTheme`, `initTheme` | `src/handlers/setThemeHandler.ts` | `initTheme` |
| `scrollToSection` | `src/handlers/scrollToSectionHandler.ts` | `scrollToSectionHandler` |
| `initMenuFromUrl` | `src/handlers/initMenuFromUrlHandler.ts` | `initMenuFromUrlHandler` |
| `initFilterVisibility`, `saveFilterVisibility`, `toggleFilterVisibility`, `resetFilterVisibility` | `src/handlers/filterVisibilityHandler.ts` | `initFilterVisibilityHandler` |
| `saveMultilingualTag`, `cancelMultilingualTag`, `updateMultilingualTagValue` | `src/handlers/multilingualTagHandler.ts` | `saveMultilingualTagHandler` |
---
### 주의사항
```text
이 핸들러들은 sirsoft-admin_basic 템플릿에서만 등록됨 (다른 템플릿에서 미지원 가능)
범용 핸들러(navigate, apiCall, setState 등)와 달리 템플릿 의존적
✅ 커스텀 핸들러이므로 template.json의 핸들러 등록 확인 필요
✅ 범용 핸들러는 actions-handlers.md 참조
```
---
### 관련 문서
- [액션 핸들러 개요](../../../../docs/frontend/actions-handlers.md)
- [sirsoft-admin_basic 컴포넌트](components.md)
- [sirsoft-admin_basic 레이아웃](layouts.md)
- [sirsoft-basic 핸들러](../../../../docs/frontend/templates/sirsoft-basic/handlers.md)
@@ -0,0 +1,621 @@
# Admin Basic — 레이아웃
> 레이아웃 목록과 라우트 매핑 · 진입점: [AGENTS.md](../AGENTS.md)
## 레이아웃 목록
<!-- @generated:layouts START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
레이아웃 145개 (루트: `layouts`).
| 그룹 | 개수 |
|---|---|
| `(root)` | 27개 |
| `auth` | 1개 |
| `errors` | 6개 |
| `overrides` | 1개 |
| `partials` | 110개 |
| 레이아웃 | 그룹 | 종류 | extends |
|---|---|---|---|
| `_admin_base` | `(root)` | partial | - |
| `admin_activity_log_list` | `(root)` | 화면 | `_admin_base` |
| `admin_dashboard` | `(root)` | 화면 | `_admin_base` |
| `admin_forgot_password` | `(root)` | 화면 | - |
| `admin_identity_logs` | `(root)` | 화면 | `_admin_base` |
| `admin_language_pack_list` | `(root)` | 화면 | `_admin_base` |
| `admin_language_packs` | `(root)` | 화면 | `admin_language_pack_list` |
| `admin_language_packs_install_modal` | `(root)` | 화면 | - |
| `admin_login` | `(root)` | 화면 | - |
| `admin_menu_list` | `(root)` | 화면 | `_admin_base` |
| `admin_module_language_packs` | `(root)` | 화면 | `admin_language_pack_list` |
| `admin_module_list` | `(root)` | 화면 | `_admin_base` |
| `admin_notification_log_list` | `(root)` | 화면 | `_admin_base` |
| `admin_plugin_language_packs` | `(root)` | 화면 | `admin_language_pack_list` |
| `admin_plugin_list` | `(root)` | 화면 | `_admin_base` |
| `admin_reset_password` | `(root)` | 화면 | - |
| `admin_role_form` | `(root)` | 화면 | `_admin_base` |
| `admin_role_list` | `(root)` | 화면 | `_admin_base` |
| `admin_schedule_list` | `(root)` | 화면 | `_admin_base` |
| `admin_settings` | `(root)` | 화면 | `_admin_base` |
| `admin_template_language_packs` | `(root)` | 화면 | `admin_language_pack_list` |
| `admin_template_layout_edit` | `(root)` | 화면 | `_admin_base` |
| `admin_template_list` | `(root)` | 화면 | `_admin_base` |
| `admin_user_detail` | `(root)` | 화면 | `_admin_base` |
| `admin_user_form` | `(root)` | 화면 | `_admin_base` |
| `admin_user_list` | `(root)` | 화면 | `_admin_base` |
| `identity_challenge` | `auth` | 화면 | `_admin_base` |
| `401` | `errors` | 화면 | `_admin_base` |
| `403` | `errors` | 화면 | `_admin_base` |
| `404` | `errors` | 화면 | `_admin_base` |
| `500` | `errors` | 화면 | `_admin_base` |
| `503` | `errors` | 화면 | `_admin_base` |
| `maintenance` | `errors` | 화면 | `_admin_base` |
| `index` | `overrides` | 화면 | `_admin_base` |
| `_identity_challenge_modal` | `partials` | partial | - |
| `_modal_changelog` | `partials` | partial | - |
| `_modal_license` | `partials` | partial | - |
| `_modal_notification_delete_all_confirm` | `partials` | partial | - |
| `_partial_datagrid` | `partials` | partial | - |
| `_partial_filter` | `partials` | partial | - |
| `_content` | `partials` | partial | - |
| `_modal_log_detail` | `partials` | partial | - |
| `_modal_purge_confirm` | `partials` | partial | - |
| `_partial_datagrid` | `partials` | partial | - |
| `_partial_filter` | `partials` | partial | - |
| `_content` | `partials` | partial | - |
| `_drawer_manifest_preview` | `partials` | partial | - |
| `_modal_detail` | `partials` | partial | - |
| `_modal_install` | `partials` | partial | - |
| `_modal_install_bundled` | `partials` | partial | - |
| `_modal_refresh_cache` | `partials` | partial | - |
| `_modal_slot_conflict` | `partials` | partial | - |
| `_modal_uninstall` | `partials` | partial | - |
| `_modal_update` | `partials` | partial | - |
| `_modal_delete` | `partials` | partial | - |
| `_panel_detail` | `partials` | partial | - |
| `_panel_form` | `partials` | partial | - |
| `_panel_menu_list` | `partials` | partial | - |
| `_panel_view` | `partials` | partial | - |
| `_drawer_manifest_preview` | `partials` | partial | - |
| `_modal_deactivate_warning` | `partials` | partial | - |
| `_modal_detail` | `partials` | partial | - |
| `_modal_extension_license` | `partials` | partial | - |
| `_modal_force_activate` | `partials` | partial | - |
| `_modal_force_deactivate` | `partials` | partial | - |
| `_modal_install` | `partials` | partial | - |
| `_modal_manual_install` | `partials` | partial | - |
| `_modal_reactivate_language_packs` | `partials` | partial | - |
| `_modal_refresh_layouts` | `partials` | partial | - |
| `_modal_uninstall` | `partials` | partial | - |
| `_modal_update` | `partials` | partial | - |
| `_modal_log_detail` | `partials` | partial | - |
| `_partial_datagrid` | `partials` | partial | - |
| `_partial_filter` | `partials` | partial | - |
| `_drawer_manifest_preview` | `partials` | partial | - |
| `_modal_detail` | `partials` | partial | - |
| `_modal_extension_license` | `partials` | partial | - |
| `_modal_force_activate` | `partials` | partial | - |
| `_modal_force_deactivate` | `partials` | partial | - |
| `_modal_install` | `partials` | partial | - |
| `_modal_manual_install` | `partials` | partial | - |
| `_modal_reactivate_language_packs` | `partials` | partial | - |
| `_modal_refresh_layouts` | `partials` | partial | - |
| `_modal_uninstall` | `partials` | partial | - |
| `_modal_update` | `partials` | partial | - |
| `_modal_delete` | `partials` | partial | - |
| `_modal_delete` | `partials` | partial | - |
| `_modal_duplicate` | `partials` | partial | - |
| `_modal_form` | `partials` | partial | - |
| `_modal_history` | `partials` | partial | - |
| `_modal_run` | `partials` | partial | - |
| `_tab_schedules` | `partials` | partial | - |
| `_modal_cache_delete` | `partials` | partial | - |
| `_modal_core_changelog` | `partials` | partial | - |
| `_modal_core_update_guide` | `partials` | partial | - |
| `_modal_core_update_result` | `partials` | partial | - |
| `_modal_identity_message_definition_add` | `partials` | partial | - |
| `_modal_identity_message_definition_delete` | `partials` | partial | - |
| `_modal_identity_message_definition_reset` | `partials` | partial | - |
| `_modal_identity_message_template_form` | `partials` | partial | - |
| `_modal_identity_message_template_preview` | `partials` | partial | - |
| `_modal_identity_policy_delete` | `partials` | partial | - |
| `_modal_identity_policy_form` | `partials` | partial | - |
| `_modal_mail_template_form` | `partials` | partial | - |
| `_modal_notification_definition_reset` | `partials` | partial | - |
| `_modal_notification_template_form` | `partials` | partial | - |
| `_modal_notification_template_preview` | `partials` | partial | - |
| `_modal_password_confirm` | `partials` | partial | - |
| `_tab_advanced` | `partials` | partial | - |
| `_tab_drivers` | `partials` | partial | - |
| `_tab_general` | `partials` | partial | - |
| `_tab_identity` | `partials` | partial | - |
| `_tab_identity_basic` | `partials` | partial | - |
| `_tab_identity_messages` | `partials` | partial | - |
| `_tab_identity_policies` | `partials` | partial | - |
| `_tab_identity_providers` | `partials` | partial | - |
| `_tab_info` | `partials` | partial | - |
| `_tab_language_packs` | `partials` | partial | - |
| `_tab_mail` | `partials` | partial | - |
| `_tab_mail_templates` | `partials` | partial | - |
| `_tab_notification_definitions` | `partials` | partial | - |
| `_tab_security` | `partials` | partial | - |
| `_tab_seo` | `partials` | partial | - |
| `_tab_upload` | `partials` | partial | - |
| `_modal_extension_preview_layout` | `partials` | partial | - |
| `_modal_extension_version_history` | `partials` | partial | - |
| `_modal_version_history` | `partials` | partial | - |
| `_drawer_manifest_preview` | `partials` | partial | - |
| `_modal_activate` | `partials` | partial | - |
| `_modal_deactivate` | `partials` | partial | - |
| `_modal_detail` | `partials` | partial | - |
| `_modal_extension_license` | `partials` | partial | - |
| `_modal_force_activate` | `partials` | partial | - |
| `_modal_install` | `partials` | partial | - |
| `_modal_manual_install` | `partials` | partial | - |
| `_modal_reactivate_language_packs` | `partials` | partial | - |
| `_modal_refresh_layouts` | `partials` | partial | - |
| `_modal_uninstall` | `partials` | partial | - |
| `_modal_update` | `partials` | partial | - |
| `_tab_admin` | `partials` | partial | - |
| `_tab_user` | `partials` | partial | - |
| `_content_section` | `partials` | partial | - |
| `_header_section` | `partials` | partial | - |
| `_info_card` | `partials` | partial | - |
| `template_partial_test` | `(root)` | 화면 | `_admin_base` |
<!-- @generated:layouts END -->
<!-- @intent START -->
그룹은 "이 파일이 독립된 화면 URL을 갖는가"로 나뉩니다 — `(root)`/`auth`/`errors`/`overrides` 는
라우트에 직접 매핑되는 화면이고, `partials`(110개, 전체의 76%)는 화면에 `extends`/포함되는
조각(탭·모달·패널)이라 그 자체로는 URL이 없습니다. partial 비중이 압도적으로 큰 것은 관리자
화면 하나가 보통 탭 여러 개 + 모달 여러 개로 구성되기 때문입니다(예: 확장 관리 화면 하나가
설치/삭제/업데이트/강제활성화 등 모달을 5~10개씩 갖습니다).
이 목록은 코드에서 실측되므로 구체적인 개수·파일명을 프로즈에 하드코딩해 반복하지 않습니다
— 과거 버전 문서가 손으로 그린 전체 페이지 트리(약 20개 화면 기준)는 이미 145개로 늘어난
현재 상태와 크게 어긋나 있었습니다. 전체 목록은 항상 위 생성 표를 신뢰합니다.
### 화면 유형별 구성 패턴
새 관리자 화면을 만들 때 아래 패턴 중 가장 가까운 것을 참고합니다(구체적 파일명이 아니라
**구조**를 재사용하는 것이 목적입니다 — 실제 예시 파일은 위 표에서 비슷한 이름을 찾습니다).
**목록 화면**: `extends: _admin_base`, `slots.content` 에 `PageHeader`(제목·액션) →
`FilterGroup`(선택) → `DataGrid`/`CardGrid`(목록) → `Pagination`. data_sources 는
`auto_fetch: true` + `params`에 `_local.page`/`per_page` 바인딩. 삭제·상태변경은 `apiCall`,
상세/수정 이동은 `navigate`, 필터·페이지네이션은 `setState`. 목록 컨텍스트 왕복 규약
(`mergeQuery`)을 지킵니다(§CLAUDE.md).
**상세 화면**: `extends: _admin_base`, `data_sources`에 `route.id` 기반 상세 API,
`slots.content`에 `PageHeader` + `Card` 여러 개(기본 정보/활동 내역/권한 정보 등 섹션별 분리).
**폼 화면(생성/수정 겸용)**: `route.id` 존재 여부로 생성/수정을 분기, `Form` 안에
`FormField`+`Input`/`Select`/`Toggle` 조합. `Button` 은 `type="button"` 명시(submit 방지),
서버 검증 에러는 `FormField` 의 `error` prop 으로 표시.
**설정(탭) 화면**: `TabNavigation` + `activeTab` 상태에 따라 `_tab_*.json` partial 을
조건부 렌더링. 탭 전환은 `setState`, 저장은 `apiCall`, 파괴적 동작 확인은 `openModal`.
**확장 관리 화면**(모듈/플러그인/템플릿 목록): `DataGrid`/`CardGrid` + `StatusBadge`(상태) +
`ActionMenu`(설치/활성화/비활성화/삭제/업데이트) + `ExtensionBadge`. 설치·삭제·업데이트마다
전용 확인 모달(`_modal_install`/`_modal_uninstall`/`_modal_update` 등)을 개별 파일로 분리 —
한 모달에 여러 동작을 조건 분기로 몰아넣지 않습니다(모달마다 문구·부작용이 다릅니다).
**에러 화면**(`errors/*.json`): `extends` 없는 독립 레이아웃. `Div`(중앙 정렬) 안에
`Icon`+`H1`(코드)+`P`(메시지)+`Button`(홈 이동). 독립 레이아웃이므로 `Toast`/`Modal` 같은
전역 호스트 컴포넌트가 필요하면 직접 마운트해야 합니다(§CLAUDE.md "독립 레이아웃의 글로벌
호스트 컴포넌트").
### `_admin_base.json` 에 대한 정정
과거 버전 문서는 `_admin_base.json` 이 `init_actions: [initTheme, initMenuFromUrl]` 를 갖는다고
적었으나, 현재 `_admin_base.json` 에는 `init_actions` 키 자체가 없습니다 — 두 핸들러는 현재
`_admin_base` 를 상속하지 않는 인증 화면(`admin_login`/`admin_forgot_password`/
`admin_reset_password`)에서만 호출됩니다. 사이드바 접힘 상태 복원(`initSidebar`)은 레이아웃이
아니라 템플릿 부트스트랩(`src/index.ts`)에서 직접 호출됩니다. `_admin_base` 를 고칠 때 이
문서의 낡은 구조를 그대로 믿지 말고 실제 JSON 을 확인하세요.
<!-- @intent END -->
## 라우트 매핑
<!-- @generated:layout-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 경로 | 레이아웃 | 이름 |
|---|---|---|
| `*/admin` | `-` | - |
| `*/admin/login` | `admin_login` | - |
| `*/admin/forgot-password` | `admin_forgot_password` | - |
| `*/admin/reset-password` | `admin_reset_password` | - |
| `*/admin/dashboard` | `admin_dashboard` | - |
| `*/admin/users` | `admin_user_list` | - |
| `*/admin/users/create` | `admin_user_form` | - |
| `*/admin/users/:id` | `admin_user_detail` | - |
| `*/admin/users/:id/edit` | `admin_user_form` | - |
| `*/admin/modules` | `admin_module_list` | - |
| `*/admin/settings/language-packs` | `-` | - |
| `*/admin/modules/:identifier/language-packs` | `admin_module_language_packs` | - |
| `*/admin/plugins/:identifier/language-packs` | `admin_plugin_language_packs` | - |
| `*/admin/templates/:identifier/language-packs` | `admin_template_language_packs` | - |
| `*/admin/plugins` | `admin_plugin_list` | - |
| `*/admin/menus` | `admin_menu_list` | - |
| `*/admin/templates` | `-` | - |
| `*/admin/templates/:identifier/edit` | `admin_template_layout_edit` | - |
| `*/admin/templates/:type` | `admin_template_list` | - |
| `*/admin/template/partial` | `template_partial_test` | - |
| `*/admin/activity-logs` | `admin_activity_log_list` | - |
| `*/admin/notification-logs` | `admin_notification_log_list` | - |
| `*/admin/settings` | `admin_settings` | - |
| `*/admin/roles` | `admin_role_list` | - |
| `*/admin/roles/create` | `admin_role_form` | - |
| `*/admin/roles/:id/edit` | `admin_role_form` | - |
| `*/admin/schedules` | `admin_schedule_list` | - |
| `*/admin/identity/logs` | `admin_identity_logs` | - |
| `*/admin/identity/challenge` | `auth/identity_challenge` | - |
<!-- @generated:layout-map END -->
<!-- @intent START -->
`레이아웃` 열이 `-` 인 두 행(`*/admin`, `*/admin/settings/language-packs`)은 레이아웃이
없다는 뜻이 아니라 **다른 라우트로 리다이렉트되는 진입점**입니다 — 예를 들어 `/admin` 은
로그인 여부에 따라 `/admin/login` 또는 `/admin/dashboard` 로 넘어가는 게이트 라우트입니다.
새 화면을 추가할 때 이 표에 라우트를 등록하는 것만으로 끝나지 않습니다 — 사이드바 메뉴에서
그 화면으로 이동하는 진입점도 함께 추가해야 실제로 도달 가능해집니다(라우트만 있고 메뉴
항목이 없으면 URL을 직접 입력해야만 닿는 화면이 됩니다).
<!-- @intent END -->
## 확장 오버라이드
<!-- @generated:template-overrides START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
_오버라이드하는 레이아웃 확장 조각이 없습니다._
<!-- @generated:template-overrides END -->
<!-- @intent START -->
이 템플릿은 현재 어떤 모듈/플러그인의 레이아웃 확장 조각도 오버라이드하지 않습니다 —
`sirsoft-basic` 템플릿과 달리 관리자 화면은 확장이 끼워 넣는 조각(예: 이커머스 문의 설정,
GDPR 배너)을 코어 대시보드 위젯 형태로만 받고, 이 템플릿이 그 조각을 대체할 필요가 아직
없었기 때문입니다. 특정 확장의 관리자 UI 를 이 템플릿에서만 다르게 보이게 하려면
`extensions/{module-identifier}/*.json` 을 신설합니다(§docs/extension/layout-extensions.md
"템플릿 오버라이드").
<!-- @intent END -->
## 이관 원문 상세
> 아래는 코어 `docs/frontend/templates/sirsoft-admin_basic/layouts.md` 에 있던 원문을
> 이 문서로 옮긴 것입니다(#601). 페이지 맵 트리와 화면 유형별 패턴 상세가 여기에 있습니다.
> 이관 시점 그대로 보존하되, 현재 코드와 어긋나는 부분에는 정정 주석을 달았습니다 —
> 레이아웃·라우트의 SSoT 는 위 「레이아웃 목록」·「라우트 매핑」 블록입니다.
### 페이지 맵 (트리 구조)
```text
_admin_base.json (베이스 레이아웃)
│
├── admin_dashboard.json (대시보드)
├── admin_login.json (로그인 — _admin_base 미상속)
│
├── 사용자 관리
│ ├── admin_user_list.json (목록)
│ ├── admin_user_form.json (생성/수정)
│ └── admin_user_detail.json (상세)
│
├── 역할 관리
│ ├── admin_role_list.json (목록)
│ │ └── partials/admin_role_list/_modal_delete.json
│ └── admin_role_form.json (생성/수정)
│
├── 메뉴 관리
│ └── admin_menu_list.json (3패널 레이아웃)
│ └── partials/admin_menu_list/
│ ├── _panel_menu_list.json (좌측: 메뉴 트리)
│ ├── _panel_form.json (중앙: 편집 폼)
│ ├── _panel_detail.json (중앙: 상세 보기)
│ ├── _panel_view.json (우측: 미리보기)
│ └── _modal_delete.json
│
├── 환경 설정
│ └── admin_settings.json (탭 네비게이션)
│ └── partials/admin_settings/
│ ├── _tab_general.json (일반)
│ ├── _tab_mail.json (메일 발송 SMTP 설정)
│ ├── _tab_notification_definitions.json (알림 정의)
│ ├── _tab_security.json (보안)
│ ├── _tab_upload.json (업로드)
│ ├── _tab_drivers.json (드라이버)
│ ├── _tab_seo.json (SEO)
│ ├── _tab_advanced.json (고급)
│ ├── _tab_info.json (시스템 정보)
│ ├── _modal_cache_delete.json
│ ├── _modal_core_changelog.json
│ ├── _modal_core_update_guide.json
│ ├── _modal_core_update_result.json
│ ├── _modal_notification_template_form.json
│ ├── _modal_notification_template_preview.json
│ └── _modal_password_confirm.json
│
├── 모듈 관리
│ └── admin_module_list.json
│ └── partials/admin_module_list/
│ ├── _modal_detail.json
│ ├── _modal_install.json
│ ├── _modal_manual_install.json
│ ├── _modal_uninstall.json
│ ├── _modal_update.json
│ ├── _modal_deactivate_warning.json
│ ├── _modal_force_activate.json
│ ├── _modal_force_deactivate.json
│ ├── _modal_extension_license.json
│ └── _modal_refresh_layouts.json
│
├── 플러그인 관리
│ └── admin_plugin_list.json
│ └── partials/admin_plugin_list/
│ ├── (모듈과 동일 구조 — 10개 모달)
│ └── ...
│
├── 템플릿 관리
│ ├── admin_template_list.json
│ │ └── partials/admin_template_list/
│ │ ├── _tab_admin.json (Admin 템플릿 탭)
│ │ ├── _tab_user.json (User 템플릿 탭)
│ │ ├── _modal_detail.json
│ │ ├── _modal_install.json
│ │ ├── _modal_manual_install.json
│ │ ├── _modal_uninstall.json
│ │ ├── _modal_update.json
│ │ ├── _modal_activate.json
│ │ ├── _modal_deactivate.json
│ │ ├── _modal_force_activate.json
│ │ ├── _modal_extension_license.json
│ │ └── _modal_refresh_layouts.json
│ └── admin_template_layout_edit.json (레이아웃 편집기)
│ └── partials/admin_template_layout_edit/_modal_version_history.json
│
├── 스케줄 관리
│ └── admin_schedule_list.json
│ └── partials/admin_schedule_list/
│ ├── _tab_schedules.json
│ ├── _modal_form.json
│ ├── _modal_delete.json
│ ├── _modal_duplicate.json
│ ├── _modal_history.json
│ └── _modal_run.json
│
├── 메일 발송 로그
│ └── admin_mail_send_log_list.json
│ └── partials/admin_mail_send_log_list/
│ ├── _partial_datagrid.json
│ └── _partial_filter.json
│
├── 공통 Partial
│ ├── partials/_modal_changelog.json
│ └── partials/_modal_license.json
│
├── 에러 페이지
│ └── errors/
│ ├── 401.json (인증 필요)
│ ├── 403.json (접근 거부)
│ ├── 404.json (페이지 없음)
│ ├── 500.json (서버 오류)
│ ├── 503.json (서비스 불가)
│ └── maintenance.json (점검 중)
│
├── 오버라이드
│ └── overrides/sirsoft-sample/index.json
│
└── 테스트
└── template_partial_test.json
└── partials/template_partial_test/
├── _content_section.json
├── _header_section.json
└── _info_card.json
```
---
### 카테고리별 가이드
#### 목록 페이지 패턴
**대표**: `admin_user_list.json`, `admin_role_list.json`
**구성**:
```text
extends: _admin_base
slots.content:
└── PageHeader (제목, 액션 버튼)
└── FilterGroup (선택적)
└── DataGrid (columns, data, pagination)
└── Pagination
```
**data_sources**:
```json
{
"id": "users",
"endpoint": "/api/admin/users",
"method": "GET",
"auto_fetch": true,
"params": { "page": "{{_local.page ?? 1}}", "per_page": "{{_local.per_page ?? 15}}" }
}
```
**핸들러 패턴**:
- `apiCall` — 삭제, 상태 변경
- `navigate` — 상세/수정 페이지 이동
- `setState` — 필터, 페이지네이션 상태
**Partial 구조**: 모달 (삭제 확인, 상세 보기 등)
---
#### 상세 페이지 패턴
**대표**: `admin_user_detail.json`
**구성**:
```text
extends: _admin_base
data_sources: [상세 API (route.id 기반)]
slots.content:
└── PageHeader
└── Card (기본 정보 섹션)
└── Card (활동 내역 섹션)
└── Card (권한 정보 섹션)
```
**data_sources**:
```json
{
"id": "user",
"endpoint": "/api/admin/users/{{route.id}}",
"method": "GET",
"auto_fetch": true
}
```
**핸들러 패턴**:
- `apiCall` — 상태 변경 (활성화/비활성화, 역할 변경)
- `navigate` — 목록으로 이동, 수정 페이지 이동
---
#### 폼 페이지 패턴
**대표**: `admin_user_form.json`, `admin_role_form.json`
**구성**:
```text
extends: _admin_base
data_sources: [상세 API (수정 시), 참조 데이터 (Select 옵션)]
slots.content:
└── PageHeader
└── Form
├── FormField + Input (텍스트)
├── FormField + Select (선택)
├── FormField + Toggle (토글)
└── Button (저장/취소)
```
**핸들러 패턴**:
- `apiCall` — 생성 (POST) / 수정 (PUT)
- `navigate` — 성공 후 목록/상세로 이동
**주의사항**:
```text
✅ Form 내 Button에 type="button" 명시 (submit 방지)
✅ 수정 폼은 route.id 존재 여부로 생성/수정 구분
✅ FormField에 error prop으로 서버 검증 에러 표시
```
---
#### 설정 페이지 패턴
**대표**: `admin_settings.json`
**구성**:
```text
extends: _admin_base
data_sources: [설정 API]
slots.content:
└── PageHeader
└── TabNavigation (tabs)
└── Div (탭별 partial 조건부 렌더링)
├── if: activeTab === 'general' → partial: _tab_general.json
├── if: activeTab === 'mail' → partial: _tab_mail.json
├── if: activeTab === 'security' → partial: _tab_security.json
└── ...
```
**Partial 구조**: `partials/admin_settings/_tab_*.json` (9개 탭)
**핸들러 패턴**:
- `setState` — 탭 전환
- `apiCall` — 설정 저장
- `openModal` — 확인 다이얼로그
---
#### 확장 관리 패턴
**대표**: `admin_module_list.json`, `admin_plugin_list.json`, `admin_template_list.json`
**구성**:
```text
extends: _admin_base
data_sources: [확장 목록 API]
slots.content:
└── PageHeader (새로고침, 수동 설치 버튼)
└── DataGrid/CardGrid (확장 목록)
├── StatusBadge (상태)
├── ActionMenu (설치/활성화/비활성화/삭제/업데이트)
└── ExtensionBadge (모듈 식별)
modals:
├── _modal_detail.json (상세 정보)
├── _modal_install.json (설치 확인)
├── _modal_uninstall.json (삭제 확인)
├── _modal_update.json (업데이트)
└── _modal_force_activate.json 등
```
**핸들러 패턴**:
- `apiCall` — 설치, 활성화, 비활성화, 삭제, 업데이트
- `openModal` — 확인 다이얼로그
- `setState` — 선택된 확장 정보 저장
**특수사항**:
- 템플릿 관리는 Admin/User 탭 분리 (`_tab_admin.json`, `_tab_user.json`)
- 레이아웃 편집기 (`admin_template_layout_edit.json`)는 CodeEditor + 실시간 미리보기
---
#### 에러 페이지 패턴
**대표**: `errors/404.json`
**구성**:
```text
(extends 없음 — 독립 레이아웃)
components:
└── Div (전체 화면 중앙 정렬)
├── Icon (에러 아이콘)
├── H1 (에러 코드)
├── P (에러 메시지)
└── Button (홈으로 이동)
```
**핸들러 패턴**:
- `navigate` — 대시보드/홈으로 이동
---
### 베이스 레이아웃 구조
#### _admin_base.json
모든 관리자 페이지의 공통 구조를 정의합니다.
```text
_admin_base.json
├── init_actions: [initTheme, initMenuFromUrl]
├── data_sources: [admin_menu, notifications]
├── components:
│ ├── AdminSidebar (menu: admin_menu.data)
│ ├── AdminHeader (user, notifications)
│ ├── Toast
│ ├── PageTransitionIndicator
│ └── Div (content area)
│ └── slot: "content" (← 하위 레이아웃이 채움)
└── AdminFooter
```
**슬롯**:
- `content` — 각 페이지의 메인 콘텐츠가 삽입되는 위치
---
### 관련 문서
- [sirsoft-admin_basic 컴포넌트](components.md)
- [sirsoft-admin_basic 핸들러](handlers.md)
- [레이아웃 JSON 스키마](../../../../docs/frontend/layout-json.md)
- [레이아웃 상속](../../../../docs/frontend/layout-json-inheritance.md)
- [sirsoft-basic 레이아웃](../../../../docs/frontend/templates/sirsoft-basic/layouts.md)