2단계 인증은 7.0.6 에서 서버측이 갖춰졌지만 인증번호를 입력할 화면이 어느 버전에도
없었다. 그래서 그 설정을 켠 사이트는 관리자를 포함한 전원이 로그인할 수 없었다.
원인은 `POST /api/auth/login` 이 조건에 따라 **다른 형태의 200** 을 돌려준다는 것이다.
평소에는 `{token, user}` 지만 2단계 인증이 켜져 있으면 `{two_factor_required,
challenge_id, ...}` 를 돌려준다. 프론트는 앞의 형태만 선언하고 `response.data.user.language`
를 바로 읽었으므로 그 자리에서 TypeError 가 났고, 영문 원문이 로그인 화면에 그대로 노출됐다.
서버는 정상 응답했으므로 서버 로그에는 아무 흔적도 남지 않는다.
이어서 `setToken(undefined)` 가 `localStorage` 에 문자열 `"undefined"` 를 남겼다.
이 값은 truthy 라 이후 모든 요청이 `Bearer undefined` 로 나가 401 이 되고, 사용자에게는
「세션이 만료되었습니다」로 보인다. 관리자 로그인은 한발 더 나가 `null->isAdmin` 으로
500 이 되어, 설정을 되돌릴 수단까지 함께 사라졌다.
## 구현
- 로그인 응답을 판별 유니온(`LoginResult`)으로 표현하고, 형태를 판별한 뒤에 읽는다.
`ApiClient.setToken` 은 비어 있지 않은 문자열만 저장한다.
- 사용자·관리자 로그인 화면에 인증번호 입력 단계를 추가했다. 같은 카드 안에서 넘어가며
「인증번호 다시 받기」와 「처음부터」를 제공한다. 관리자 판정은 코드 확인에 성공한 뒤에
수행하고, 거부할 때는 그 직전에 발급된 토큰을 회수한다.
- 재발송(`login/two-factor/resend`)은 기존 challenge 를 취소하고 새로 발행한다. 유효한
코드를 여러 개 살려 두면 대입 시도의 표적이 넓어진다.
- 인증번호를 보내지 못하면 401 이 아니라 503 으로 답한다. 자격 증명은 올바른데 401 로
뭉개면 사용자는 비밀번호를 의심하며 같은 시도를 반복하고, 운영자는 메일 설정이 깨진
사실을 알 방법이 없다.
- 공개 본인인증 경로(`identity/verify`·`cancel`)가 로그인 목적의 challenge 를 소진하지
못하도록 403 게이트를 세웠다. 소진되면 그 challenge 로 영영 로그인할 수 없다.
- 로그인 시도 제한 429 응답이 다국어 문구를 싣도록 했다(종전에는 프레임워크 기본 영문).
- 다국어 파라미터에서 파이프 표현식이 평가되지 않아 「유효시간 까지」처럼 값이 빠지던
문제를 함께 고쳤다. 같은 결함이 문의 목록 화면에도 있었다.
## 이번 점검에서 함께 고친 것
- 계정 잠금(423)·발송 실패(503) 응답이 사용자·관리자 컨트롤러에 동일하게 복제돼 있었고
그 주석 자신은 "단일 지점에서 만든다" 고 적혀 있었다. 페이로드에 필드가 하나 추가되면
한쪽만 따라가 같은 실패를 두 화면이 다르게 안내하게 된다 — 트레이트로 통합했다.
- 테스트가 개발자 자신의 사이트 설정을 읽고 있었다. 2단계 인증을 켜 둔 환경에서는 로그인
성공을 전제한 테스트가 503 으로 깨지는데 실패 메시지가 원인을 가리키지도 않는다.
같은 결함군을 위해 이미 존재하던 단일 지점에 그 축을 추가했다.
## 버전
코어 7.0.11 · sirsoft-basic 1.1.4 · sirsoft-admin_basic 1.0.9 ·
번들 일본어팩 3종 · 템플릿 엔진 engine-v1.65.0.
28 KiB
Admin Basic — 레이아웃
레이아웃 목록과 라우트 매핑 · 진입점: AGENTS.md
레이아웃 목록
레이아웃 146개 (루트: layouts).
| 그룹 | 개수 |
|---|---|
(root) |
27개 |
auth |
1개 |
errors |
6개 |
overrides |
1개 |
partials |
111개 |
| 레이아웃 | 그룹 | 종류 | 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 | - |
_modal_static_cache_republish |
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 |
그룹은 "이 파일이 독립된 화면 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)을 지킵니다(§코어 AGENTS.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 같은
전역 호스트 컴포넌트가 필요하면 직접 마운트해야 합니다(§코어 AGENTS.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 을 확인하세요.
admin_login.json 의 2단계 인증(인증번호) 단계
보안 설정에서 2단계 인증을 켠 사이트에서는 관리자 로그인 응답도 두 형태로 갈립니다 — 정상
로그인과 인증번호 요구(challenge)입니다. admin_login.json 은 그 둘을 같은 카드 안에서 단계
전환으로 처리합니다. 이 화면은 _admin_base 를 상속하지 않는 독립 레이아웃이라 모달을 쓸 수
없고, Toast 호스트도 이 레이아웃이 직접 마운트합니다.
- 1단계(이메일·비밀번호) 블록은
if: "{{!_local.twoFactor?.required}}", 2단계(인증번호) 블록은 그 부정형입니다. 두if는 언제나 상보여야 합니다. 상태는_global이 아니라_local입니다. - 제출 시퀀스의
login과loginTwoFactor도 같은 쌍으로 상호배타입니다.loginTwoFactor쪽if가 빠지면 인증번호 단계에서 Enter 를 눌렀을 때 새 challenge 가 발급되어 흐름이 끊깁니다. - 인증번호 입력은 controlled 입니다(
value+onChange의setState쌍).events: {}래퍼는 쓰지 않습니다. - 화면 진입 시
init_actions의setState가twoFactor를null로 되돌립니다. - 관리자가 아닌 계정이 인증번호를 맞춰도 서버가 403(
auth.admin_required)으로 거부하고 이미 발급한 토큰을 회수합니다. 그 문구는loginTwoFactor의onError가 인증번호 오류 자리에 그대로 싣습니다 — 별도 분기를 두지 않습니다. - 만료 시각은 정적 표기입니다(
| datetime). 카운트다운을 쓰지 않습니다.
금지 — 같은 시퀀스·onSuccess 안에서 방금 저장한 상태를 다시 읽지 않습니다
setState 직후 형제 액션의 if 나 값으로 그 상태를 재독하면 갱신 이전 값을 읽습니다. 그 자리에서는
{{response.*}} 만 씁니다. 오류도 경고도 남지 않고 분기만 조용히 어긋납니다.
라우트 매핑
| 경로 | 레이아웃 | 이름 |
|---|---|---|
*/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 |
- |
레이아웃 열이 - 인 두 행(*/admin, */admin/settings/language-packs)은 레이아웃이
없다는 뜻이 아니라 다른 라우트로 리다이렉트되는 진입점입니다 — 예를 들어 /admin 은
로그인 여부에 따라 /admin/login 또는 /admin/dashboard 로 넘어가는 게이트 라우트입니다.
새 화면을 추가할 때 이 표에 라우트를 등록하는 것만으로 끝나지 않습니다 — 사이드바 메뉴에서
그 화면으로 이동하는 진입점도 함께 추가해야 실제로 도달 가능해집니다(라우트만 있고 메뉴
항목이 없으면 URL을 직접 입력해야만 닿는 화면이 됩니다).
확장 오버라이드
오버라이드하는 레이아웃 확장 조각이 없습니다.
이 템플릿은 현재 어떤 모듈/플러그인의 레이아웃 확장 조각도 오버라이드하지 않습니다 —
sirsoft-basic 템플릿과 달리 관리자 화면은 확장이 끼워 넣는 조각(예: 이커머스 문의 설정,
GDPR 배너)을 코어 대시보드 위젯 형태로만 받고, 이 템플릿이 그 조각을 대체할 필요가 아직
없었기 때문입니다. 특정 확장의 관리자 UI 를 이 템플릿에서만 다르게 보이게 하려면
extensions/{module-identifier}/*.json 을 신설합니다(§docs/extension/layout-extensions.md
"템플릿 오버라이드").
이관 원문 상세
아래는 코어
docs/frontend/templates/sirsoft-admin_basic/layouts.md에 있던 원문을 이 문서로 옮긴 것입니다(#601). 페이지 맵 트리와 화면 유형별 패턴 상세가 여기에 있습니다. 이관 시점 그대로 보존하되, 현재 코드와 어긋나는 부분에는 정정 주석을 달았습니다 — 레이아웃·라우트의 SSoT 는 위 「레이아웃 목록」·「라우트 매핑」 블록입니다.
페이지 맵 (트리 구조)
_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
구성:
extends: _admin_base
slots.content:
└── PageHeader (제목, 액션 버튼)
└── FilterGroup (선택적)
└── DataGrid (columns, data, pagination)
└── Pagination
data_sources:
{
"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
구성:
extends: _admin_base
data_sources: [상세 API (route.id 기반)]
slots.content:
└── PageHeader
└── Card (기본 정보 섹션)
└── Card (활동 내역 섹션)
└── Card (권한 정보 섹션)
data_sources:
{
"id": "user",
"endpoint": "/api/admin/users/{{route.id}}",
"method": "GET",
"auto_fetch": true
}
핸들러 패턴:
apiCall— 상태 변경 (활성화/비활성화, 역할 변경)navigate— 목록으로 이동, 수정 페이지 이동
폼 페이지 패턴
대표: admin_user_form.json, admin_role_form.json
구성:
extends: _admin_base
data_sources: [상세 API (수정 시), 참조 데이터 (Select 옵션)]
slots.content:
└── PageHeader
└── Form
├── FormField + Input (텍스트)
├── FormField + Select (선택)
├── FormField + Toggle (토글)
└── Button (저장/취소)
핸들러 패턴:
apiCall— 생성 (POST) / 수정 (PUT)navigate— 성공 후 목록/상세로 이동
주의사항:
✅ Form 내 Button에 type="button" 명시 (submit 방지)
✅ 수정 폼은 route.id 존재 여부로 생성/수정 구분
✅ FormField에 error prop으로 서버 검증 에러 표시
설정 페이지 패턴
대표: admin_settings.json
구성:
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
구성:
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
구성:
(extends 없음 — 독립 레이아웃)
components:
└── Div (전체 화면 중앙 정렬)
├── Icon (에러 아이콘)
├── H1 (에러 코드)
├── P (에러 메시지)
└── Button (홈으로 이동)
핸들러 패턴:
navigate— 대시보드/홈으로 이동
베이스 레이아웃 구조
_admin_base.json
모든 관리자 페이지의 공통 구조를 정의합니다.
_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— 각 페이지의 메인 콘텐츠가 삽입되는 위치