{ "version": "1.0.0", "layout_name": "admin_settings", "permissions": [ "core.settings.read" ], "extends": "_admin_base", "meta": { "title": "$t:admin.settings.page_title", "description": "$t:admin.settings.page_description" }, "computed": { "isReadOnly": "{{!!_global.settingsAbilities && !(_global.settingsAbilities?.can_update ?? false)}}" }, "transition_overlay": { "target": "settings_tab_content", "fallback_target": "right_content_area", "wait_for": [ "settings", "systemInfo", "appKey", "notificationDefinitions", "availableChannels", "identityProviders", "identityPurposes", "identityMessages", "adminIdentityPolicies", "policies", "language_packs", "sitemap_status" ] }, "data_sources": [ { "id": "language_packs", "label_key": "$t:editor.data_source.language_packs", "type": "api", "endpoint": "/api/admin/language-packs", "method": "GET", "if": "{{(query.tab || _global.activeSettingsTab || 'general') === 'language_packs'}}", "auto_fetch": true, "auth_required": true, "params": { "page": "{{query.page}}", "per_page": "{{query.per_page ?? 10}}", "scope": "{{_local.filterScope || query.scope}}", "target_identifier": "{{_local.filterTarget || query.target_identifier}}", "locale": "{{query.locale}}", "status": "{{query.status}}", "search": "{{query.search}}", "exclude_protected": "{{query.exclude_protected ?? '1'}}" }, "fallback": { "data": { "data": [], "meta": { "total": 0, "active": 0, "installed": 0, "inactive": 0, "error": 0 }, "abilities": {} } } }, { "id": "language_pack_detail", "label_key": "$t:editor.data_source.language_pack_detail", "type": "api", "endpoint": "/api/admin/language-packs/{{_local.detailId}}", "method": "GET", "auto_fetch": false, "auth_required": true, "depends_on": [ "_local.detailId" ] }, { "id": "language_pack_changelog", "label_key": "$t:editor.data_source.language_pack_changelog", "type": "api", "endpoint": "/api/admin/language-packs/{{_local.detailId}}/changelog", "method": "GET", "auto_fetch": false, "auth_required": true, "depends_on": [ "_local.detailId" ] }, { "id": "settings", "label_key": "$t:editor.data_source.settings", "type": "api", "endpoint": "/api/admin/settings", "method": "GET", "auto_fetch": true, "auth_required": true, "initLocal": { "form": "{{data}}", "originalDrivers": "drivers" }, "initGlobal": { "settingsAbilities": "abilities" }, "refetchOnMount": true, "fallback": { "data": [] } }, { "id": "systemInfo", "label_key": "$t:editor.data_source.systemInfo", "type": "api", "endpoint": "/api/admin/settings/system-info", "method": "GET", "if": "{{(query.tab || _global.activeSettingsTab || 'general') === 'info'}}", "auto_fetch": true, "auth_required": true, "fallback": { "data": { "php_version": "-", "laravel_version": "-", "database_type": "-", "server_software": "-" } } }, { "id": "appKey", "label_key": "$t:editor.data_source.appKey", "type": "api", "endpoint": "/api/admin/settings/app-key", "method": "GET", "if": "{{(query.tab || _global.activeSettingsTab || 'general') === 'security'}}", "auto_fetch": true, "auth_required": true, "fallback": { "data": { "key": "-" } } }, { "id": "notificationDefinitions", "label_key": "$t:editor.data_source.notificationDefinitions", "type": "api", "endpoint": "/api/admin/notification-definitions", "method": "GET", "if": "{{(query.tab || _global.activeSettingsTab || 'general') === 'notification_definitions'}}", "auto_fetch": true, "auth_required": true, "params": { "page": "{{query.page || 1}}", "per_page": "{{query.per_page || 20}}", "search": "{{query.search}}", "search_type": "{{query.search_type}}", "extension_identifier": "core", "template_channel": "{{query.channel ?? 'mail'}}" }, "fallback": { "data": { "data": [], "pagination": [] } } }, { "id": "availableChannels", "label_key": "$t:editor.data_source.availableChannels", "type": "api", "endpoint": "/api/admin/notification-channels", "method": "GET", "if": "{{(query.tab || _global.activeSettingsTab || 'general') === 'notification_definitions'}}", "auto_fetch": true, "auth_required": true, "fallback": { "data": [] } }, { "id": "identityProviders", "label_key": "$t:editor.data_source.identityProviders", "type": "api", "endpoint": "/api/admin/identity/providers", "method": "GET", "if": "{{(query.tab || _global.activeSettingsTab || 'general') === 'identity'}}", "auto_fetch": true, "auth_required": true, "fallback": { "data": [] } }, { "id": "identityPurposes", "label_key": "$t:editor.data_source.identityPurposes", "type": "api", "endpoint": "/api/identity/purposes", "method": "GET", "if": "{{(query.tab || _global.activeSettingsTab || 'general') === 'identity'}}", "auto_fetch": true, "fallback": { "data": [] } }, { "id": "identityMessages", "label_key": "$t:editor.data_source.identityMessages", "type": "api", "endpoint": "/api/admin/identity/messages/definitions", "method": "GET", "if": "{{(query.tab || _global.activeSettingsTab || 'general') === 'identity'}}", "auto_fetch": true, "auth_required": true, "params": { "page": "{{query.messages_page || 1}}", "per_page": "{{query.messages_per_page || 20}}", "sort_by": "id", "sort_order": "asc" }, "fallback": { "data": { "data": [] } } }, { "id": "adminIdentityPolicies", "label_key": "$t:editor.data_source.adminIdentityPolicies", "type": "api", "endpoint": "/api/admin/identity/policies", "method": "GET", "if": "{{(query.tab || _global.activeSettingsTab || 'general') === 'identity'}}", "auto_fetch": true, "auth_required": true, "params": { "source_type": "admin", "per_page": 100 }, "fallback": { "data": { "data": [] } } }, { "id": "policies", "label_key": "$t:editor.data_source.policies", "type": "api", "endpoint": "/api/admin/identity/policies", "method": "GET", "if": "{{(query.tab || _global.activeSettingsTab || 'general') === 'identity'}}", "auto_fetch": true, "auth_required": true, "params": { "scope": "{{query.scope}}", "purpose": "{{query.purpose}}", "source_type": "{{query.source_type}}", "enabled": "{{query.enabled}}", "search": "{{query.search}}", "page": "{{query.page || 1}}", "per_page": "{{query.per_page || 10}}" }, "fallback": { "data": { "data": [], "meta": {} } } }, { "id": "sitemap_status", "label_key": "$t:editor.data_source.sitemap_status", "type": "api", "endpoint": "/api/admin/seo/sitemap/status", "method": "GET", "if": "{{(query.tab || _global.activeSettingsTab || 'general') === 'seo'}}", "auto_fetch": true, "auth_required": true, "fallback": { "data": { "last_updated_at": null, "progress": null, "realtime_enabled": false } }, "onSuccess": [ { "handler": "startInterval", "if": "{{response?.data?.data?.realtime_enabled !== true && ['queued','running','writing'].includes(response?.data?.data?.progress?.status ?? '')}}", "params": { "id": "sitemap_progress_poll", "intervalMs": 3000, "actions": [ { "handler": "refetchDataSource", "params": { "dataSourceId": "sitemap_status" } } ] } }, { "handler": "stopInterval", "if": "{{['completed','failed'].includes(response?.data?.data?.progress?.status ?? '')}}", "params": { "id": "sitemap_progress_poll" } } ] }, { "id": "sitemap_progress_ws", "label_key": "$t:editor.data_source.sitemap_progress_ws", "type": "websocket", "channel": "core.admin.seo.sitemap", "event": "sitemap.progress.updated", "channel_type": "private", "target_source": "sitemap_status" } ], "slots": { "content": [ { "id": "settings_content", "type": "basic", "name": "Div", "dataKey": "form", "trackChanges": true, "props": { "className": "admin-page-content-responsive" }, "children": [ { "id": "page_header", "type": "composite", "name": "PageHeader", "props": { "title": "$t:admin.settings.page_title" }, "children": [ { "id": "refresh_button", "type": "basic", "name": "Button", "props": { "type": "button", "className": "btn-icon" }, "actions": [ { "type": "click", "handler": "refetchDataSource", "params": { "dataSourceId": "settings" } } ], "children": [ { "type": "basic", "name": "Icon", "props": { "name": "refresh", "className": "text-xl" } } ] } ] }, { "id": "readonly_banner", "type": "basic", "name": "Div", "if": "{{_computed.isReadOnly}}", "props": { "className": "alert-warning" }, "children": [ { "type": "basic", "name": "Div", "props": { "className": "flex-center gap-3" }, "children": [ { "type": "basic", "name": "Icon", "props": { "name": "lock", "className": "text-xl text-yellow-600 dark:text-yellow-400 flex-shrink-0" } }, { "type": "basic", "name": "P", "props": { "className": "text-warning-soft" }, "text": "$t:admin.settings.readonly_notice" } ] } ] }, { "id": "sticky_header", "type": "basic", "name": "Div", "props": { "className": "sticky-tab-nav-responsive" }, "children": [ { "id": "tab_navigation", "type": "composite", "name": "TabNavigation", "props": { "className": "sticky-tab-nav-responsive", "tabs": [ { "id": "general", "label": "$t:admin.settings.tabs.general" }, { "id": "mail", "label": "$t:admin.settings.tabs.mail" }, { "id": "upload", "label": "$t:admin.settings.tabs.upload" }, { "id": "seo", "label": "$t:admin.settings.tabs.seo" }, { "id": "security", "label": "$t:admin.settings.tabs.security" }, { "id": "drivers", "label": "$t:admin.settings.tabs.drivers" }, { "id": "advanced", "label": "$t:admin.settings.tabs.advanced" }, { "id": "notification_definitions", "label": "$t:admin.settings.tabs.notification_definitions" }, { "id": "language_packs", "label": "$t:admin.settings.tabs.language_packs" }, { "id": "identity", "label": "$t:admin.settings.tabs.identity" }, { "id": "info", "label": "$t:admin.settings.tabs.info" } ], "activeTabId": "{{_global.activeSettingsTab || query.tab || 'general'}}", "variant": "default" }, "actions": [ { "event": "onTabChange", "type": "change", "handler": "sequence", "params": { "actions": [ { "handler": "setState", "params": { "target": "local", "errors": null } }, { "handler": "setState", "params": { "target": "global", "activeSettingsTab": "{{$args[0]}}" } }, { "comment": "audit:allow layout-list-context-navigate-merge-query 탭 전환 — 탭마다 다루는 대상이 달라 이전 탭의 목록 상태를 물려받으면 오히려 잘못된 화면이 된다.", "handler": "navigate", "params": { "path": "/admin/settings", "query": { "tab": "{{$args[0]}}" }, "replace": true } } ] } } ] }, { "id": "identity_sub_tab_navigation", "type": "composite", "name": "TabNavigationScroll", "if": "{{(_global.activeSettingsTab || query.tab) === 'identity'}}", "props": { "tabs": [ { "id": "basic", "label": "$t:admin.settings.identity.sub_tabs.basic" }, { "id": "providers", "label": "$t:admin.settings.identity.sub_tabs.providers" }, { "id": "policies", "label": "$t:admin.settings.identity.sub_tabs.policies" }, { "id": "messages", "label": "$t:admin.settings.identity.sub_tabs.messages" } ], "className": "sub-tab-nav", "sectionIdPrefix": "identity_section_", "enableScrollSpy": true, "scrollContainerId": "right_content_area", "scrollSpyOffset": 180, "scrollOffset": 180 } } ] }, { "id": "settings_tab_content", "type": "basic", "name": "Div", "children": [ { "id": "validation_error", "type": "basic", "name": "Div", "if": "{{_local.errors}}", "props": { "className": "alert-danger" }, "children": [ { "type": "basic", "name": "Div", "props": { "className": "flex gap-3" }, "children": [ { "type": "basic", "name": "Icon", "props": { "name": "triangle-exclamation", "className": "text-xl text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" } }, { "type": "basic", "name": "Div", "children": [ { "type": "basic", "name": "H3", "props": { "className": "text-error-strong" }, "text": "$t:admin.settings.validation_error" }, { "type": "basic", "name": "P", "if": "{{typeof _local.errors === 'string'}}", "props": { "className": "text-error-soft mt-2" }, "text": "{{_local.errors}}" }, { "type": "basic", "name": "Ul", "if": "{{typeof _local.errors === 'object' && Object.keys(_local.errors).length > 0}}", "props": { "className": "mt-2 list-disc list-inside space-y-1" }, "iteration": { "source": "Object.entries(_local.errors ?? {}).flatMap(([field, messages]) => Array.isArray(messages) ? messages : [messages])", "item_var": "message" }, "children": [ { "type": "basic", "name": "Li", "props": { "className": "text-error-soft" }, "text": "{{message}}" } ] } ] } ] } ] }, { "partial": "partials/admin_settings/_tab_general.json" }, { "partial": "partials/admin_settings/_tab_mail.json" }, { "partial": "partials/admin_settings/_tab_upload.json" }, { "partial": "partials/admin_settings/_tab_seo.json" }, { "partial": "partials/admin_settings/_tab_security.json" }, { "partial": "partials/admin_settings/_tab_drivers.json" }, { "partial": "partials/admin_settings/_tab_advanced.json" }, { "partial": "partials/admin_settings/_tab_notification_definitions.json" }, { "partial": "partials/admin_settings/_tab_language_packs.json" }, { "partial": "partials/admin_settings/_tab_identity.json" }, { "partial": "partials/admin_settings/_tab_info.json" } ], "props": { "className": "tab-content" } }, { "id": "footer_buttons", "type": "basic", "name": "Div", "if": "{{!_computed.isReadOnly && (_global.activeSettingsTab || query.tab || 'general') !== 'info' && (_global.activeSettingsTab || query.tab || 'general') !== 'language_packs'}}", "props": { "className": "sticky-footer-buttons" }, "children": [ { "id": "cancel_button", "type": "basic", "name": "Button", "props": { "className": "btn btn-secondary" }, "actions": [ { "type": "click", "handler": "refetchDataSource", "params": { "dataSourceId": "settings" } } ], "children": [ { "type": "basic", "name": "Span", "text": "$t:common.cancel" } ] }, { "id": "save_button", "type": "basic", "name": "Button", "props": { "disabled": "{{!_local.hasChanges || _local.isSaving || ((_global.activeSettingsTab || query.tab) === 'drivers' && ((_local.form?.drivers?.storage_driver === 's3' && !_local.driverTestResults?.s3?.success && ((_local.form?.drivers?.storage_driver || '') !== (_local.originalDrivers?.storage_driver || '') || (_local.form?.drivers?.s3_bucket || '') !== (_local.originalDrivers?.s3_bucket || '') || (_local.form?.drivers?.s3_region || '') !== (_local.originalDrivers?.s3_region || '') || (_local.form?.drivers?.s3_access_key || '') !== (_local.originalDrivers?.s3_access_key || '') || (_local.form?.drivers?.s3_secret_key || '') !== (_local.originalDrivers?.s3_secret_key || '') || (_local.form?.drivers?.s3_endpoint || '') !== (_local.originalDrivers?.s3_endpoint || '') || Boolean(_local.form?.drivers?.s3_use_path_style) !== Boolean(_local.originalDrivers?.s3_use_path_style))) || (_local.form?.drivers?.cache_driver === 'redis' && !_local.driverTestResults?.redis?.success && ((_local.form?.drivers?.cache_driver || '') !== (_local.originalDrivers?.cache_driver || '') || (_local.form?.drivers?.redis_host || '') !== (_local.originalDrivers?.redis_host || '') || String(_local.form?.drivers?.redis_port ?? '') !== String(_local.originalDrivers?.redis_port ?? '') || (_local.form?.drivers?.redis_password || '') !== (_local.originalDrivers?.redis_password || ''))) || (_local.form?.drivers?.cache_driver === 'memcached' && !_local.driverTestResults?.memcached?.success && ((_local.form?.drivers?.cache_driver || '') !== (_local.originalDrivers?.cache_driver || '') || (_local.form?.drivers?.memcached_host || '') !== (_local.originalDrivers?.memcached_host || '') || String(_local.form?.drivers?.memcached_port ?? '') !== String(_local.originalDrivers?.memcached_port ?? ''))) || (_local.form?.drivers?.session_driver === 'redis' && !_local.driverTestResults?.redis?.success && ((_local.form?.drivers?.session_driver || '') !== (_local.originalDrivers?.session_driver || '') || (_local.form?.drivers?.redis_host || '') !== (_local.originalDrivers?.redis_host || '') || String(_local.form?.drivers?.redis_port ?? '') !== String(_local.originalDrivers?.redis_port ?? '') || (_local.form?.drivers?.redis_password || '') !== (_local.originalDrivers?.redis_password || ''))) || (_local.form?.drivers?.queue_driver === 'redis' && !_local.driverTestResults?.redis?.success && ((_local.form?.drivers?.queue_driver || '') !== (_local.originalDrivers?.queue_driver || '') || (_local.form?.drivers?.redis_host || '') !== (_local.originalDrivers?.redis_host || '') || String(_local.form?.drivers?.redis_port ?? '') !== String(_local.originalDrivers?.redis_port ?? '') || (_local.form?.drivers?.redis_password || '') !== (_local.originalDrivers?.redis_password || ''))) || (_local.form?.drivers?.websocket_enabled && !_local.driverTestResults?.websocket?.success && (Boolean(_local.form?.drivers?.websocket_enabled) !== Boolean(_local.originalDrivers?.websocket_enabled) || (_local.form?.drivers?.websocket_host || '') !== (_local.originalDrivers?.websocket_host || '') || String(_local.form?.drivers?.websocket_port ?? '') !== String(_local.originalDrivers?.websocket_port ?? '') || (_local.form?.drivers?.websocket_scheme || '') !== (_local.originalDrivers?.websocket_scheme || '')))))}}", "className": "btn btn-primary flex-center gap-1.5" }, "actions": [ { "type": "click", "handler": "sequence", "params": { "actions": [ { "handler": "emitEvent", "params": { "event": "upload:site_logo" } }, { "handler": "setState", "params": { "target": "local", "isSaving": true, "errors": null } }, { "handler": "apiCall", "auth_required": true, "target": "/api/admin/settings", "params": { "method": "POST", "body": "{{(function() { const tab = _global.activeSettingsTab || query.tab || 'general'; const form = _local.form || {}; if (tab === 'notification_definitions') { return { _tab: 'notifications', notifications: { channels: form.notifications?.channels || [] } }; } return { _tab: tab, [tab]: form[tab] || {} }; })()}}" }, "onSuccess": [ { "handler": "setState", "params": { "target": "local", "isSaving": false, "hasChanges": false } }, { "handler": "setState", "params": { "target": "global", "settings": "{{result.data.settings}}", "uploadSettings": "{{result.data.settings.upload}}" } }, { "handler": "toast", "params": { "type": "success", "message": "$t:admin.settings.save_success" } }, { "handler": "refetchDataSource", "params": { "dataSourceId": "settings" } } ], "onError": [ { "handler": "setState", "params": { "target": "local", "isSaving": false, "errors": "{{error.errors}}" } }, { "handler": "toast", "params": { "type": "error", "message": "$t:admin.settings.save_error" } } ] } ] } } ], "children": [ { "type": "basic", "name": "Icon", "if": "{{_local.isSaving}}", "props": { "name": "spinner", "className": "spinner-sm" } }, { "type": "basic", "name": "Span", "if": "{{!_local.isSaving}}", "text": "$t:common.save" }, { "type": "basic", "name": "Span", "if": "{{_local.isSaving}}", "text": "$t:common.saving" } ] } ] } ] } ] }, "modals": [ { "partial": "partials/admin_language_pack_list/_modal_install.json" }, { "partial": "partials/admin_language_pack_list/_modal_install_bundled.json" }, { "partial": "partials/admin_language_pack_list/_modal_detail.json" }, { "partial": "partials/admin_language_pack_list/_modal_update.json" }, { "partial": "partials/admin_language_pack_list/_modal_uninstall.json" }, { "partial": "partials/admin_language_pack_list/_modal_refresh_cache.json" }, { "partial": "partials/admin_language_pack_list/_drawer_manifest_preview.json" }, { "partial": "partials/admin_settings/_modal_password_confirm.json" }, { "partial": "partials/admin_settings/_modal_cache_delete.json" }, { "partial": "partials/admin_settings/_modal_notification_template_form.json" }, { "partial": "partials/admin_settings/_modal_notification_template_preview.json" }, { "partial": "partials/admin_settings/_modal_notification_definition_reset.json" }, { "partial": "partials/admin_settings/_modal_core_update_result.json" }, { "partial": "partials/admin_settings/_modal_core_changelog.json" }, { "partial": "partials/admin_settings/_modal_core_update_guide.json" }, { "partial": "partials/admin_settings/_modal_identity_policy_form.json" }, { "partial": "partials/admin_settings/_modal_identity_policy_delete.json" }, { "partial": "partials/admin_settings/_modal_identity_message_template_form.json" }, { "partial": "partials/admin_settings/_modal_identity_message_template_preview.json" }, { "partial": "partials/admin_settings/_modal_identity_message_definition_add.json" }, { "partial": "partials/admin_settings/_modal_identity_message_definition_delete.json" }, { "partial": "partials/admin_settings/_modal_identity_message_definition_reset.json" } ], "initLocal": { "driverTestResults": {}, "editingDefinition": null, "editingTemplate": null, "editLang": "ko", "previewData": null, "isSaving": false, "templateErrors": null, "checkingCoreUpdate": false, "coreUpdateInfo": null, "coreChangelog": null, "loadingChangelog": false, "policyForm": null, "policyErrors": null, "policyIsNew": true, "policyIsSaving": false }, "init_actions": [ { "handler": "setState", "params": { "target": "local", "notificationDefinitionFilter": { "search": "{{query.search || ''}}", "searchType": "{{query.search_type || 'all'}}" }, "filterScope": "{{query.scope || ''}}", "filterStatus": "{{query.status || ''}}", "filterLocale": "{{query.locale || ''}}", "filterSearch": "{{query.search || ''}}", "filterTarget": "", "scopeLocked": false } } ] }