{ "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" } } }