여러 화면에서 조건부(FormRequest) 검증이 우회되거나 과도하게 제한되던 결함을 한 계열로 묶어 수정한다. - 검증 규칙 확장 필터 이름 10개를 나머지 92개와 동일 규칙으로 통일 - 설명 길이를 바이트가 아닌 글자 수로 계산(한글 오검출 해소) - 비활성 언어팩 번역 보존한 채 수정 허용 - 알림 템플릿 제목 nullable 존중, 미지원 발송 수단 저장 차단 - 일정/실행 이력 목록 조회 검증 정합 - 게시판 첨부 미사용 시 빈 확장자 통과, new_display_hours/최소 길이 0 허용, 일괄 적용 override_values 범위 검증(SSoT) - 이커머스 클레임 사유 조회 검증, 용어 정정 - 원문 i18n 키 노출 다수 해소, 저장/삭제 실패 시 사유 화면 노출 - 아이콘 크기/다크모드 대비 회귀 수정 번들 확장(모듈3·플러그인6·템플릿2·언어팩5) 버전 new-bump 및 CHANGELOG 반영, ja 언어팩 동기.
1416 lines
91 KiB
JSON
1416 lines
91 KiB
JSON
{
|
|
"version": "1.0.0",
|
|
"layout_name": "_admin_base",
|
|
"meta": {
|
|
"title": "$t:admin.base_layout_title",
|
|
"description": "$t:admin.base_layout_description",
|
|
"is_base": true
|
|
},
|
|
"data_sources": [
|
|
{
|
|
"id": "admin_menu",
|
|
"label_key": "$t:editor.data_source.admin_menu",
|
|
"type": "api",
|
|
"endpoint": "/api/admin/menus/active",
|
|
"method": "GET",
|
|
"auto_fetch": true,
|
|
"auth_required": true,
|
|
"loading_strategy": "progressive"
|
|
},
|
|
{
|
|
"id": "current_user",
|
|
"label_key": "$t:editor.data_source.current_user",
|
|
"type": "api",
|
|
"endpoint": "/api/admin/auth/user",
|
|
"method": "GET",
|
|
"auto_fetch": true,
|
|
"auth_required": true,
|
|
"loading_strategy": "progressive"
|
|
},
|
|
{
|
|
"id": "notifications",
|
|
"label_key": "$t:editor.data_source.notifications",
|
|
"type": "api",
|
|
"endpoint": "/api/admin/notifications",
|
|
"method": "GET",
|
|
"auto_fetch": true,
|
|
"auth_required": true,
|
|
"loading_strategy": "progressive",
|
|
"params": {
|
|
"per_page": 15,
|
|
"read": "{{(_global.notificationsUnreadOnly ?? true) ? 'unread' : 'all'}}"
|
|
}
|
|
},
|
|
{
|
|
"id": "notification_unread_count",
|
|
"label_key": "$t:editor.data_source.notification_unread_count",
|
|
"type": "api",
|
|
"endpoint": "/api/admin/notifications/unread-count",
|
|
"method": "GET",
|
|
"auto_fetch": true,
|
|
"auth_required": true,
|
|
"loading_strategy": "progressive",
|
|
"initGlobal": {
|
|
"key": "adminNotificationCount",
|
|
"path": "unread_count"
|
|
}
|
|
},
|
|
{
|
|
"id": "installed_modules",
|
|
"label_key": "$t:editor.data_source.installed_modules",
|
|
"type": "api",
|
|
"endpoint": "/api/admin/modules/installed",
|
|
"method": "GET",
|
|
"auto_fetch": true,
|
|
"auth_required": true,
|
|
"loading_strategy": "progressive",
|
|
"initGlobal": {
|
|
"key": "installedModules",
|
|
"path": "data"
|
|
}
|
|
},
|
|
{
|
|
"id": "installed_plugins",
|
|
"label_key": "$t:editor.data_source.installed_plugins",
|
|
"type": "api",
|
|
"endpoint": "/api/admin/plugins/installed",
|
|
"method": "GET",
|
|
"auto_fetch": true,
|
|
"auth_required": true,
|
|
"loading_strategy": "progressive",
|
|
"initGlobal": {
|
|
"key": "installedPlugins",
|
|
"path": "data"
|
|
}
|
|
},
|
|
{
|
|
"id": "notification_ws",
|
|
"label_key": "$t:editor.data_source.notification_ws",
|
|
"type": "websocket",
|
|
"channel": "core.user.notifications.{{current_user.data.uuid}}",
|
|
"event": "notification.received",
|
|
"channel_type": "private",
|
|
"target_source": "notification_unread_count",
|
|
"onReceive": [
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notification_unread_count"
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notifications"
|
|
}
|
|
},
|
|
{
|
|
"handler": "toast",
|
|
"params": {
|
|
"type": "info",
|
|
"message": "{{$args[0].subject ?? '$t:admin.new_notification_received'}}",
|
|
"duration": 5000
|
|
}
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"components": [
|
|
{
|
|
"comment": "통화 선택기 슬롯 주입 앵커 — 이커머스 모듈이 layout_extensions 로 'header_currency' 슬롯 노드를 여기에 주입한다(slot 노드는 원위치 렌더 안 함, SlotContext 등록만 → header_right 의 SlotContainer 가 떙겨 렌더). 미설치 시 빈 슬롯. 헤더는 주입 모듈을 모른다.",
|
|
"id": "header_currency_inject_anchor",
|
|
"type": "basic",
|
|
"name": "Div"
|
|
},
|
|
{
|
|
"id": "global_toast",
|
|
"type": "composite",
|
|
"name": "Toast",
|
|
"props": {
|
|
"toasts": "{{_global.toasts}}",
|
|
"position": "bottom-center",
|
|
"duration": 5000
|
|
}
|
|
},
|
|
{
|
|
"id": "page_transition_indicator",
|
|
"type": "composite",
|
|
"name": "PageTransitionIndicator"
|
|
},
|
|
{
|
|
"id": "layout_warnings",
|
|
"type": "composite",
|
|
"name": "LayoutWarnings",
|
|
"props": {
|
|
"warnings": "{{_global.layoutWarnings}}",
|
|
"className": "layout_warnings"
|
|
}
|
|
},
|
|
{
|
|
"id": "admin_layout_root",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "admin_layout_root"
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "mobile_overlay",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"if": "{{false}}",
|
|
"props": {
|
|
"className": "mobile_overlay"
|
|
},
|
|
"responsive": {
|
|
"portable": {
|
|
"if": "{{_global.sidebarOpen}}"
|
|
}
|
|
},
|
|
"actions": [
|
|
{
|
|
"type": "click",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"sidebarOpen": false
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "left_sidebar_area",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "left_sidebar_area {{_global.sidebarCollapsed ? 'left_sidebar_area_collapsed' : ''}}"
|
|
},
|
|
"responsive": {
|
|
"portable": {
|
|
"props": {
|
|
"className": "left_sidebar_area_portable {{_global.sidebarOpen ? 'left_sidebar_area_portable_open' : ''}}"
|
|
}
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "sidebar_header",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "sidebar_header"
|
|
},
|
|
"responsive": {
|
|
"portable": {
|
|
"props": {
|
|
"className": "hidden"
|
|
}
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "sidebar_logo",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "sidebar_logo"
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "logo_image",
|
|
"type": "basic",
|
|
"name": "Img",
|
|
"if": "{{_global.settings?.general?.site_logo_url}}",
|
|
"props": {
|
|
"src": "{{_global.settings?.general?.site_logo_url}}",
|
|
"alt": "{{_global.settings?.general?.site_name ?? '그누보드7'}}",
|
|
"className": "logo_image"
|
|
}
|
|
},
|
|
{
|
|
"id": "app_name",
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"if": "{{!_global.settings?.general?.site_logo_url}}",
|
|
"props": {
|
|
"className": "text-primary font-bold text-xl truncate"
|
|
},
|
|
"text": "{{_global.settings?.general?.site_name ?? '그누보드7'}}"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"comment": "언어/통화/배송국가 선택 — 모바일 전용. 헤더 우측 그룹에서 이동(모바일 헤더가 420px 를 요구해 폰 뷰포트에서 통화 선택기가 화면 밖으로 잘렸다). 사용자 템플릿 _user_base 의 mobile_drawer_prefs 와 동일하게 셋 다 가로 나열 칩으로 통일. 통화/배송국가는 이커머스 모듈이 'header_currency' 슬롯에 주입하므로 SlotContainer 만 배치(모듈 비활성 시 자동 미렌더). 스크롤 영역(admin_sidebar_menu) 바깥 최상단에 둔다.",
|
|
"id": "admin_drawer_prefs",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"if": "{{false}}",
|
|
"responsive": {
|
|
"portable": {
|
|
"if": "{{true}}"
|
|
}
|
|
},
|
|
"props": {
|
|
"className": "px-4 py-3 border-b border-gray-200 dark:border-gray-700 space-y-3"
|
|
},
|
|
"children": [
|
|
{
|
|
"comment": "언어 선택 (여러 언어 지원 시) — 헤더 LanguageSelector 드롭다운에서 가로 칩으로 전환. 접기/펼치기: 접힘이 기본이고 트리거에 현재 언어를 요약 표기한다. 통화/배송국가(이커머스 주입 슬롯)와는 독립된 토글(_global.mobileLanguageOpen)이라 서로 영향 없음.",
|
|
"id": "admin_drawer_language",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"if": "{{$locales.length > 1}}",
|
|
"children": [
|
|
{
|
|
"comment": "트리거 — 좌측 '언어' 라벨, 우측 현재 언어명 + chevron. 접힘 상태에서도 현재 값을 알 수 있어야 하므로 요약을 항상 노출한다.",
|
|
"id": "admin_drawer_language_toggle",
|
|
"type": "basic",
|
|
"name": "Button",
|
|
"props": {
|
|
"type": "button",
|
|
"className": "w-full flex items-center justify-between py-1 cursor-pointer",
|
|
"aria-expanded": "{{_global.mobileLanguageOpen ?? false}}"
|
|
},
|
|
"actions": [
|
|
{
|
|
"type": "click",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"mobileLanguageOpen": "{{!_global.mobileLanguageOpen}}"
|
|
}
|
|
}
|
|
],
|
|
"children": [
|
|
{
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"props": {
|
|
"className": "text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"
|
|
},
|
|
"text": "$t:admin.language"
|
|
},
|
|
{
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"props": {
|
|
"className": "flex items-center gap-1.5 text-sm text-gray-700 dark:text-gray-300"
|
|
},
|
|
"children": [
|
|
{
|
|
"comment": "접힘 상태 요약 = 현재 언어명",
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"props": { "className": "font-medium" },
|
|
"text": "{{_global.appConfig?.localeNames?.[$locale] ?? $locale}}"
|
|
},
|
|
{
|
|
"type": "basic",
|
|
"name": "Icon",
|
|
"props": {
|
|
"name": "chevron-down",
|
|
"className": "{{_global.mobileLanguageOpen ? 'rotate-180' : ''}} text-xs text-gray-400 dark:text-gray-500 transition-transform duration-200"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"comment": "펼침 영역 — max-h/opacity 트랜지션. overflow-hidden 이라 접힘 시 칩이 잘려 사라진다.",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "overflow-hidden transition-all duration-300 ease-in-out {{_global.mobileLanguageOpen ? 'max-h-screen opacity-100 mt-2' : 'max-h-0 opacity-0 mt-0'}}"
|
|
},
|
|
"children": [
|
|
{
|
|
"comment": "가로 나열 칩. iteration 은 반드시 Button 자신에 건다 — 엔진은 iteration 을 가진 노드를 항목 수만큼 복제하므로, flex 컨테이너에 걸면 컨테이너가 복제되어 칩이 세로로 쌓인다.",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "flex flex-wrap gap-2"
|
|
},
|
|
"children": [
|
|
{
|
|
"type": "basic",
|
|
"name": "Button",
|
|
"iteration": { "source": "{{$locales}}", "item_var": "loc" },
|
|
"props": {
|
|
"type": "button",
|
|
"className": "{{loc === $locale ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800' : 'text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700'}} inline-flex items-center gap-1.5 px-3 py-1.5 text-sm border rounded-full cursor-pointer whitespace-nowrap",
|
|
"role": "option",
|
|
"aria-selected": "{{loc === $locale}}"
|
|
},
|
|
"actions": [
|
|
{ "type": "click", "handler": "setLocale", "target": "{{loc}}" }
|
|
],
|
|
"children": [
|
|
{ "type": "basic", "name": "Icon", "props": { "name": "globe", "size": "sm" } },
|
|
{ "type": "basic", "name": "Span", "text": "{{_global.appConfig?.localeNames?.[loc] ?? loc}}" }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"comment": "통화/배송국가 선택 — 이커머스 모듈 주입 슬롯. id 필수: SlotContainer 는 주입 컴포넌트 root id 를 {id}__{containerId} 로 스코프한다. className 은 block(헤더의 flex items-center 를 쓰면 주입 루트가 콘텐츠 폭으로 줄어든다). 주입 루트가 portable 에서 스스로 w-full 을 갖는다.",
|
|
"id": "admin_drawer_currency_wrap",
|
|
"type": "composite",
|
|
"name": "SlotContainer",
|
|
"props": {
|
|
"slotId": "header_currency",
|
|
"className": "block"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "admin_sidebar_menu",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "admin_sidebar_menu sidebar-scrollbar"
|
|
},
|
|
"responsive": {
|
|
"portable": {
|
|
"props": {
|
|
"className": "admin_sidebar_menu_portable sidebar-scrollbar"
|
|
}
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "admin_sidebar",
|
|
"type": "composite",
|
|
"name": "AdminSidebar",
|
|
"if": "{{(admin_menu.data ?? []).length > 0}}",
|
|
"props": {
|
|
"menu": "{{admin_menu.data}}"
|
|
},
|
|
"data_binding": {
|
|
"menu": "admin_menu.data"
|
|
}
|
|
},
|
|
{
|
|
"id": "admin_sidebar_empty",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"if": "{{admin_menu.success === true && (admin_menu.data ?? []).length === 0}}",
|
|
"props": {
|
|
"className": "admin_sidebar_empty"
|
|
},
|
|
"children": [
|
|
{
|
|
"type": "basic",
|
|
"name": "I",
|
|
"props": {
|
|
"className": "admin_sidebar_empty_icon fas fa-lock"
|
|
}
|
|
},
|
|
{
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"text": "$t:admin.sidebar.no_accessible_menus"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "sidebar_footer",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "sidebar_footer"
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "sidebar_view_site_link",
|
|
"type": "basic",
|
|
"name": "A",
|
|
"props": {
|
|
"href": "/",
|
|
"target": "_blank",
|
|
"rel": "noopener noreferrer",
|
|
"title": "$t:admin.sidebar.view_site",
|
|
"className": "sidebar_view_site_link"
|
|
},
|
|
"children": [
|
|
{
|
|
"type": "basic",
|
|
"name": "I",
|
|
"props": {
|
|
"className": "sidebar_view_site_icon fas fa-globe"
|
|
}
|
|
},
|
|
{
|
|
"id": "sidebar_view_site_label",
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"props": {
|
|
"className": "sidebar_view_site_label"
|
|
},
|
|
"text": "$t:admin.sidebar.view_site"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "right_content_wrapper",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "right_content_wrapper"
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "main_header",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "main_header"
|
|
},
|
|
"responsive": {
|
|
"portable": {
|
|
"props": {
|
|
"className": "hidden"
|
|
}
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "header_left",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "header_left"
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "menu_toggle_btn",
|
|
"type": "basic",
|
|
"name": "Button",
|
|
"props": {
|
|
"type": "button",
|
|
"className": "menu_toggle_btn"
|
|
},
|
|
"actions": [
|
|
{
|
|
"type": "click",
|
|
"handler": "toggleSidebar"
|
|
}
|
|
],
|
|
"children": [
|
|
{
|
|
"type": "basic",
|
|
"name": "I",
|
|
"props": {
|
|
"className": "menu_toggle_btn_icon fas fa-bars"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "header_right",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "header_right"
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "theme_toggle_desktop",
|
|
"type": "composite",
|
|
"name": "ThemeToggle",
|
|
"props": {
|
|
"autoText": "$t:admin.theme.auto",
|
|
"lightText": "$t:admin.theme.light",
|
|
"darkText": "$t:admin.theme.dark"
|
|
}
|
|
},
|
|
{
|
|
"comment": "언어 선택 — 헤더 독립 버튼(코어 기능). UserProfile 드롭다운에도 있으나, 통화와 함께 헤더에 노출(유저 템플릿과 패리티). showCode 로 globe 아이콘 + 현재 로케일 코드를 표시(유저 헤더와 동일 표기).",
|
|
"id": "language_selector_desktop",
|
|
"type": "composite",
|
|
"name": "LanguageSelector",
|
|
"props": {
|
|
"availableLocales": "{{_global.appConfig.supportedLocales ?? $locales}}",
|
|
"languageText": "$t:admin.language",
|
|
"showCode": true
|
|
}
|
|
},
|
|
{
|
|
"comment": "통화 선택 — 이커머스 모듈이 'header_currency' 슬롯에 주입(layout_extensions). 헤더는 슬롯 이름만 알고 통화/모듈을 모름. 모듈 비활성 시 빈 슬롯 → 미렌더(인프라 자동 게이트).",
|
|
"id": "header_currency_slot_desktop",
|
|
"type": "composite",
|
|
"name": "SlotContainer",
|
|
"props": {
|
|
"slotId": "header_currency",
|
|
"className": "flex items-center"
|
|
}
|
|
},
|
|
{
|
|
"id": "notification_center_desktop",
|
|
"type": "composite",
|
|
"name": "NotificationCenter",
|
|
"props": {
|
|
"notifications": "{{(notifications?.data?.data ?? []).map(n => ({ id: n.id, title: n.subject ?? '', message: n.body ?? '', time: n.created_at ?? '', read: !!n.read_at, url: n.url ?? '', data: n.data ?? {} }))}}",
|
|
"unreadCount": "{{_global.adminNotificationCount ?? 0}}",
|
|
"hasMore": "{{_global.notificationsHasMore ?? notifications?.data?.pagination?.has_more_pages ?? false}}",
|
|
"loading": "{{_global.notificationsLoadingMore ?? notifications?.loading ?? false}}",
|
|
"titleText": "$t:admin.notifications",
|
|
"emptyText": "$t:admin.no_notifications",
|
|
"markAllReadText": "$t:admin.notifications_mark_all_read",
|
|
"deleteAllText": "$t:admin.notifications_delete_all",
|
|
"unreadOnlyText": "$t:admin.notifications_unread_only",
|
|
"unreadOnly": "{{_global.notificationsUnreadOnly ?? true}}",
|
|
"dropdownAlign": "right"
|
|
},
|
|
"actions": [
|
|
{
|
|
"event": "onClose",
|
|
"handler": "sequence",
|
|
"params": {
|
|
"actions": [
|
|
{
|
|
"handler": "apiCall",
|
|
"auth_required": true,
|
|
"target": "/api/admin/notifications/read-batch",
|
|
"params": {
|
|
"method": "POST",
|
|
"body": {
|
|
"ids": "{{$args[0]}}"
|
|
}
|
|
},
|
|
"onSuccess": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsCurrentPage": 1,
|
|
"notificationsHasMore": null,
|
|
"notificationsLoadingMore": false
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notifications"
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notification_unread_count"
|
|
}
|
|
}
|
|
],
|
|
"onError": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsLoadingMore": false
|
|
}
|
|
},
|
|
{
|
|
"handler": "toast",
|
|
"params": {
|
|
"type": "error",
|
|
"message": "{{error.message}}"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"event": "onNotificationClick",
|
|
"handler": "parallel",
|
|
"params": {
|
|
"actions": [
|
|
{
|
|
"handler": "navigate",
|
|
"params": {
|
|
"path": "{{$args[0].url ?? '/admin/notification-logs'}}"
|
|
}
|
|
},
|
|
{
|
|
"handler": "apiCall",
|
|
"auth_required": true,
|
|
"target": "/api/admin/notifications/{{$args[0].id}}/read",
|
|
"params": {
|
|
"method": "PATCH"
|
|
},
|
|
"onSuccess": [
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notification_unread_count"
|
|
}
|
|
}
|
|
],
|
|
"onError": [
|
|
{
|
|
"handler": "toast",
|
|
"params": {
|
|
"type": "error",
|
|
"message": "{{error.message}}"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"event": "onMarkAllRead",
|
|
"handler": "sequence",
|
|
"params": {
|
|
"actions": [
|
|
{
|
|
"handler": "apiCall",
|
|
"auth_required": true,
|
|
"target": "/api/admin/notifications/read-all",
|
|
"params": {
|
|
"method": "POST"
|
|
},
|
|
"onSuccess": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsCurrentPage": 1,
|
|
"notificationsHasMore": null,
|
|
"notificationsLoadingMore": false
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notifications"
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notification_unread_count"
|
|
}
|
|
}
|
|
],
|
|
"onError": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsLoadingMore": false
|
|
}
|
|
},
|
|
{
|
|
"handler": "toast",
|
|
"params": {
|
|
"type": "error",
|
|
"message": "{{error.message}}"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"event": "onUnreadOnlyToggle",
|
|
"handler": "sequence",
|
|
"params": {
|
|
"actions": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsUnreadOnly": "{{$args[0]}}",
|
|
"notificationsCurrentPage": 1,
|
|
"notificationsHasMore": null,
|
|
"notificationsLoadingMore": false
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notifications"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"event": "onDelete",
|
|
"handler": "apiCall",
|
|
"auth_required": true,
|
|
"target": "/api/admin/notifications/{{$args[0].id}}",
|
|
"params": {
|
|
"method": "DELETE"
|
|
},
|
|
"onSuccess": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsCurrentPage": 1,
|
|
"notificationsHasMore": null
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notifications"
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notification_unread_count"
|
|
}
|
|
}
|
|
],
|
|
"onError": [
|
|
{
|
|
"handler": "toast",
|
|
"params": {
|
|
"type": "error",
|
|
"message": "{{error.message}}"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"event": "onDeleteAll",
|
|
"handler": "openModal",
|
|
"target": "notification_delete_all_confirm_modal"
|
|
},
|
|
{
|
|
"event": "onLoadMore",
|
|
"handler": "sequence",
|
|
"params": {
|
|
"actions": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsLoadingMore": true
|
|
}
|
|
},
|
|
{
|
|
"handler": "apiCall",
|
|
"auth_required": true,
|
|
"target": "/api/admin/notifications",
|
|
"params": {
|
|
"method": "GET",
|
|
"query": {
|
|
"page": "{{(_global.notificationsCurrentPage ?? notifications?.data?.pagination?.current_page ?? 1) + 1}}",
|
|
"per_page": 15,
|
|
"read": "{{(_global.notificationsUnreadOnly ?? true) ? 'unread' : 'all'}}"
|
|
}
|
|
},
|
|
"onSuccess": [
|
|
{
|
|
"handler": "appendDataSource",
|
|
"params": {
|
|
"dataSourceId": "notifications",
|
|
"dataPath": "data.data",
|
|
"newData": "{{response.data.data ?? []}}"
|
|
}
|
|
},
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsCurrentPage": "{{response.data.pagination.current_page}}",
|
|
"notificationsHasMore": "{{response.data.pagination.has_more_pages}}",
|
|
"notificationsLoadingMore": false
|
|
}
|
|
}
|
|
],
|
|
"onError": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsLoadingMore": false
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "user_avatar_section",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "user_avatar_section"
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "user_avatar",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "user_avatar"
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "user_avatar_img",
|
|
"type": "basic",
|
|
"name": "Img",
|
|
"if": "{{current_user.data?.avatar_url}}",
|
|
"props": {
|
|
"src": "{{current_user.data?.avatar_url}}",
|
|
"alt": "{{current_user.data?.name ?? 'User'}}",
|
|
"className": "user_avatar_img"
|
|
}
|
|
},
|
|
{
|
|
"id": "user_avatar_fallback",
|
|
"type": "basic",
|
|
"name": "I",
|
|
"if": "{{!current_user.data?.avatar_url}}",
|
|
"props": {
|
|
"className": "fas fa-user icon-muted"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "user_info",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "user_info"
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "user_name",
|
|
"type": "basic",
|
|
"name": "P",
|
|
"props": {
|
|
"className": "text-primary text-sm font-bold leading-none"
|
|
},
|
|
"text": "{{current_user.data?.name ?? '관리자'}}"
|
|
},
|
|
{
|
|
"id": "user_email",
|
|
"type": "basic",
|
|
"name": "P",
|
|
"props": {
|
|
"className": "text-tertiary text-[11px] mt-1"
|
|
},
|
|
"text": "{{current_user.data?.email ?? ''}}"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "user_dropdown",
|
|
"type": "composite",
|
|
"name": "UserProfile",
|
|
"props": {
|
|
"user": "{{current_user.data}}",
|
|
"profileText": "$t:admin.profile_settings",
|
|
"logoutText": "$t:admin.logout",
|
|
"languageText": "$t:admin.language",
|
|
"availableLocales": "{{_global.appConfig.supportedLocales ?? $locales}}",
|
|
"className": "",
|
|
"showAvatar": false,
|
|
"showName": false,
|
|
"dropdownOnly": true,
|
|
"showChevron": false,
|
|
"dropdownDirection": "down"
|
|
},
|
|
"data_binding": {
|
|
"user": "current_user.data"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "mobile_header",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "hidden"
|
|
},
|
|
"responsive": {
|
|
"portable": {
|
|
"props": {
|
|
"className": "mobile_header_portable"
|
|
}
|
|
}
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "mobile_header_left",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "mobile_header_left"
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "mobile_menu_toggle",
|
|
"type": "basic",
|
|
"name": "Button",
|
|
"props": {
|
|
"className": "flex-center btn-icon"
|
|
},
|
|
"actions": [
|
|
{
|
|
"type": "click",
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"sidebarOpen": "{{!_global.sidebarOpen}}"
|
|
}
|
|
}
|
|
],
|
|
"children": [
|
|
{
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"props": {
|
|
"className": "text-primary text-lg leading-none"
|
|
},
|
|
"text": "☰"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "mobile_logo",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "mobile_logo"
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "mobile_logo_image",
|
|
"type": "basic",
|
|
"name": "Img",
|
|
"if": "{{_global.settings?.general?.site_logo_url}}",
|
|
"props": {
|
|
"src": "{{_global.settings?.general?.site_logo_url}}",
|
|
"alt": "{{_global.settings?.general?.site_name ?? '그누보드7'}}",
|
|
"className": "mobile_logo_image"
|
|
}
|
|
},
|
|
{
|
|
"type": "basic",
|
|
"name": "Span",
|
|
"if": "{{!_global.settings?.general?.site_logo_url}}",
|
|
"props": {
|
|
"className": "text-primary font-bold text-lg leading-none truncate"
|
|
},
|
|
"text": "{{_global.settings?.general?.site_name ?? '그누보드7'}}"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "mobile_header_right",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "mobile_header_right"
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "theme_toggle_mobile",
|
|
"type": "composite",
|
|
"name": "ThemeToggle",
|
|
"props": {
|
|
"autoText": "$t:admin.theme.auto",
|
|
"lightText": "$t:admin.theme.light",
|
|
"darkText": "$t:admin.theme.dark"
|
|
}
|
|
},
|
|
{
|
|
"id": "notification_center_mobile",
|
|
"type": "composite",
|
|
"name": "NotificationCenter",
|
|
"props": {
|
|
"notifications": "{{(notifications?.data?.data ?? []).map(n => ({ id: n.id, title: n.subject ?? '', message: n.body ?? '', time: n.created_at ?? '', read: !!n.read_at, url: n.url ?? '', data: n.data ?? {} }))}}",
|
|
"unreadCount": "{{_global.adminNotificationCount ?? 0}}",
|
|
"hasMore": "{{_global.notificationsHasMore ?? notifications?.data?.pagination?.has_more_pages ?? false}}",
|
|
"loading": "{{_global.notificationsLoadingMore ?? notifications?.loading ?? false}}",
|
|
"titleText": "$t:admin.notifications",
|
|
"emptyText": "$t:admin.no_notifications",
|
|
"markAllReadText": "$t:admin.notifications_mark_all_read",
|
|
"deleteAllText": "$t:admin.notifications_delete_all",
|
|
"unreadOnlyText": "$t:admin.notifications_unread_only",
|
|
"unreadOnly": "{{_global.notificationsUnreadOnly ?? true}}"
|
|
},
|
|
"actions": [
|
|
{
|
|
"event": "onClose",
|
|
"handler": "sequence",
|
|
"params": {
|
|
"actions": [
|
|
{
|
|
"handler": "apiCall",
|
|
"auth_required": true,
|
|
"target": "/api/admin/notifications/read-batch",
|
|
"params": {
|
|
"method": "POST",
|
|
"body": {
|
|
"ids": "{{$args[0]}}"
|
|
}
|
|
},
|
|
"onSuccess": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsCurrentPage": 1,
|
|
"notificationsHasMore": null,
|
|
"notificationsLoadingMore": false
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notifications"
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notification_unread_count"
|
|
}
|
|
}
|
|
],
|
|
"onError": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsLoadingMore": false
|
|
}
|
|
},
|
|
{
|
|
"handler": "toast",
|
|
"params": {
|
|
"type": "error",
|
|
"message": "{{error.message}}"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"event": "onNotificationClick",
|
|
"handler": "parallel",
|
|
"params": {
|
|
"actions": [
|
|
{
|
|
"handler": "navigate",
|
|
"params": {
|
|
"path": "{{$args[0].url ?? '/admin/notification-logs'}}"
|
|
}
|
|
},
|
|
{
|
|
"handler": "apiCall",
|
|
"auth_required": true,
|
|
"target": "/api/admin/notifications/{{$args[0].id}}/read",
|
|
"params": {
|
|
"method": "PATCH"
|
|
},
|
|
"onSuccess": [
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notification_unread_count"
|
|
}
|
|
}
|
|
],
|
|
"onError": [
|
|
{
|
|
"handler": "toast",
|
|
"params": {
|
|
"type": "error",
|
|
"message": "{{error.message}}"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"event": "onMarkAllRead",
|
|
"handler": "sequence",
|
|
"params": {
|
|
"actions": [
|
|
{
|
|
"handler": "apiCall",
|
|
"auth_required": true,
|
|
"target": "/api/admin/notifications/read-all",
|
|
"params": {
|
|
"method": "POST"
|
|
},
|
|
"onSuccess": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsCurrentPage": 1,
|
|
"notificationsHasMore": null,
|
|
"notificationsLoadingMore": false
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notifications"
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notification_unread_count"
|
|
}
|
|
}
|
|
],
|
|
"onError": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsLoadingMore": false
|
|
}
|
|
},
|
|
{
|
|
"handler": "toast",
|
|
"params": {
|
|
"type": "error",
|
|
"message": "{{error.message}}"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"event": "onUnreadOnlyToggle",
|
|
"handler": "sequence",
|
|
"params": {
|
|
"actions": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsUnreadOnly": "{{$args[0]}}",
|
|
"notificationsCurrentPage": 1,
|
|
"notificationsHasMore": null,
|
|
"notificationsLoadingMore": false
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notifications"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"event": "onDelete",
|
|
"handler": "apiCall",
|
|
"auth_required": true,
|
|
"target": "/api/admin/notifications/{{$args[0].id}}",
|
|
"params": {
|
|
"method": "DELETE"
|
|
},
|
|
"onSuccess": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsCurrentPage": 1,
|
|
"notificationsHasMore": null
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notifications"
|
|
}
|
|
},
|
|
{
|
|
"handler": "refetchDataSource",
|
|
"params": {
|
|
"dataSourceId": "notification_unread_count"
|
|
}
|
|
}
|
|
],
|
|
"onError": [
|
|
{
|
|
"handler": "toast",
|
|
"params": {
|
|
"type": "error",
|
|
"message": "{{error.message}}"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"event": "onDeleteAll",
|
|
"handler": "openModal",
|
|
"target": "notification_delete_all_confirm_modal"
|
|
},
|
|
{
|
|
"event": "onLoadMore",
|
|
"handler": "sequence",
|
|
"params": {
|
|
"actions": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsLoadingMore": true
|
|
}
|
|
},
|
|
{
|
|
"handler": "apiCall",
|
|
"auth_required": true,
|
|
"target": "/api/admin/notifications",
|
|
"params": {
|
|
"method": "GET",
|
|
"query": {
|
|
"page": "{{(_global.notificationsCurrentPage ?? notifications?.data?.pagination?.current_page ?? 1) + 1}}",
|
|
"per_page": 15,
|
|
"read": "{{(_global.notificationsUnreadOnly ?? true) ? 'unread' : 'all'}}"
|
|
}
|
|
},
|
|
"onSuccess": [
|
|
{
|
|
"handler": "appendDataSource",
|
|
"params": {
|
|
"dataSourceId": "notifications",
|
|
"dataPath": "data.data",
|
|
"newData": "{{response.data.data ?? []}}"
|
|
}
|
|
},
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsCurrentPage": "{{response.data.pagination.current_page}}",
|
|
"notificationsHasMore": "{{response.data.pagination.has_more_pages}}",
|
|
"notificationsLoadingMore": false
|
|
}
|
|
}
|
|
],
|
|
"onError": [
|
|
{
|
|
"handler": "setState",
|
|
"params": {
|
|
"target": "global",
|
|
"notificationsLoadingMore": false
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "right_content_area",
|
|
"type": "basic",
|
|
"name": "Div",
|
|
"props": {
|
|
"className": "right_content_area"
|
|
},
|
|
"children": [
|
|
{
|
|
"id": "main_content",
|
|
"type": "layout",
|
|
"name": "Container",
|
|
"slot": "content",
|
|
"props": {
|
|
"className": "main_content",
|
|
"layout": "flex",
|
|
"direction": "column",
|
|
"padding": 24
|
|
},
|
|
"children": []
|
|
},
|
|
{
|
|
"id": "admin_footer",
|
|
"type": "composite",
|
|
"name": "AdminFooter",
|
|
"props": {
|
|
"copyright": "© {{_global.appConfig?.releaseYear ?? '2026'}} 그누보드7",
|
|
"version": "{{_global.appConfig?.version ?? ''}}",
|
|
"copyrightModalId": "license_modal",
|
|
"changelogModalId": "changelog_modal"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"modals": [
|
|
{
|
|
"partial": "partials/_identity_challenge_modal.json"
|
|
},
|
|
{
|
|
"partial": "partials/_modal_license.json"
|
|
},
|
|
{
|
|
"partial": "partials/_modal_changelog.json"
|
|
},
|
|
{
|
|
"partial": "partials/_modal_notification_delete_all_confirm.json"
|
|
}
|
|
],
|
|
"transition_overlay": {
|
|
"enabled": true,
|
|
"style": "spinner",
|
|
"target": "right_content_area",
|
|
"spinner": {
|
|
"component": "PageLoading"
|
|
}
|
|
}
|
|
}
|