diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-asset-url-mode.test.tsx b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-asset-url-mode.test.tsx index fa607ad3..404e725b 100644 --- a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-asset-url-mode.test.tsx +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-asset-url-mode.test.tsx @@ -284,6 +284,44 @@ describe('자산 URL 이중 모드 — 레이아웃 렌더링 (§검증)', () => t.cleanup(); }); + it('에셋 서빙 방식 항목이 지역화가 아니라 전용 카드에 있다 (배치)', () => { + const fs = require('fs'); + const path = require('path'); + const json = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, + '../../layouts/partials/admin_settings/_tab_general.json', + ), + 'utf8', + ), + ); + const find = (node: any, id: string): any => { + if (!node || typeof node !== 'object') return null; + if (node.id === id) return node; + for (const k of Object.keys(node)) { + if (node[k] && typeof node[k] === 'object') { + const r = find(node[k], id); + if (r) return r; + } + } + + return null; + }; + const has = (node: any, id: string): boolean => !!find(node, id); + + const serving = find(json, 'card_asset_serving'); + expect(serving, '에셋 서빙 전용 카드가 없다').toBeTruthy(); + expect(has(serving, 'field_asset_url_mode'), '전용 카드에 항목이 없다').toBe(true); + + const localization = find(json, 'card_localization'); + expect(localization, '지역화 카드가 없다').toBeTruthy(); + expect( + has(localization, 'field_asset_url_mode'), + '에셋 서빙 방식이 여전히 지역화 카드에 있다 — 언어/시간대와 무관한 인프라 설정이다', + ).toBe(false); + }); + // 감지 결과 인라인 안내의 실제 렌더는 `_local` 조건부이며, 이 상태는 엔진의 // 로컬상태 provider 로 평가된다 — createLayoutTest 는 이를 시드하지 못하므로 // (dataContext._local 은 value 바인딩만 커버) 렌더 단언 대신 실제 레이아웃 JSON 의 diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_general.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_general.json index d84afd95..ef0b95d2 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_general.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_general.json @@ -463,137 +463,141 @@ "text": "{{_local.errors?.['general.timezone']?.[0] ?? ''}}" } ] - }, + } + ] + } + ] + } + ] + }, + { + "id": "card_asset_serving", + "type": "basic", + "name": "Div", + "props": { + "className": "admin-card" + }, + "children": [ + { + "type": "basic", + "name": "H3", + "props": { + "className": "card-title" + }, + "text": "$t:admin.settings.general.asset_url_mode" + }, + { + "type": "basic", + "name": "Div", + "props": { + "className": "card-description" + }, + "text": "$t:admin.settings.general.asset_url_mode_help" + }, + { + "id": "field_asset_url_mode", + "type": "basic", + "name": "Div", + "children": [ + { + "type": "basic", + "name": "Div", + "props": { + "className": "flex gap-2 items-start" + }, + "children": [ { - "id": "field_asset_url_mode", "type": "basic", "name": "Div", + "props": { + "className": "flex-1" + }, + "children": [ + { + "type": "composite", + "name": "Select", + "props": { + "name": "general.asset_url_mode", + "className": "w-full", + "disabled": "{{_computed.isReadOnly}}", + "options": [ + { + "value": "extension", + "label": "$t:admin.settings.general.asset_url_mode_extension" + }, + { + "value": "extensionless", + "label": "$t:admin.settings.general.asset_url_mode_extensionless" + } + ] + } + } + ] + }, + { + "id": "asset_url_mode_detect_button", + "type": "basic", + "name": "Button", + "props": { + "type": "button", + "className": "btn btn-secondary whitespace-nowrap", + "disabled": "{{_computed.isReadOnly}}" + }, "children": [ { "type": "basic", - "name": "Label", - "props": { - "className": "form-label" - }, - "children": [ - { - "type": "basic", - "name": "Span", - "text": "$t:admin.settings.general.asset_url_mode" - } - ] - }, + "name": "Span", + "text": "$t:admin.settings.general.asset_url_mode_detect" + } + ], + "actions": [ { - "type": "basic", - "name": "P", - "props": { - "className": "form-help" - }, - "text": "$t:admin.settings.general.asset_url_mode_help" - }, - { - "type": "basic", - "name": "Div", - "props": { - "className": "flex gap-2 items-start" - }, - "children": [ - { - "type": "basic", - "name": "Div", - "props": { - "className": "flex-1" - }, - "children": [ - { - "type": "composite", - "name": "Select", - "props": { - "name": "general.asset_url_mode", - "className": "w-full", - "disabled": "{{_computed.isReadOnly}}", - "options": [ - { - "value": "extension", - "label": "$t:admin.settings.general.asset_url_mode_extension" - }, - { - "value": "extensionless", - "label": "$t:admin.settings.general.asset_url_mode_extensionless" - } - ] - } - } - ] - }, - { - "id": "asset_url_mode_detect_button", - "type": "basic", - "name": "Button", - "props": { - "type": "button", - "className": "btn btn-secondary whitespace-nowrap", - "disabled": "{{_computed.isReadOnly}}" - }, - "children": [ - { - "type": "basic", - "name": "Span", - "text": "$t:admin.settings.general.asset_url_mode_detect" - } - ], - "actions": [ - { - "event": "onClick", - "handler": "detectAssetUrlMode" - } - ] - } - ] - }, - { - "id": "asset_url_mode_msg_detecting", - "type": "basic", - "name": "P", - "if": "{{_local.asset_url_mode_detect_status === 'detecting'}}", - "props": { - "className": "mt-2 text-sm text-gray-500 dark:text-gray-400" - }, - "text": "$t:admin.settings.general.asset_url_mode_detecting" - }, - { - "id": "asset_url_mode_msg_extension", - "type": "basic", - "name": "P", - "if": "{{_local.asset_url_mode_detect_status === 'extension'}}", - "props": { - "className": "mt-2 text-sm text-green-600 dark:text-green-400" - }, - "text": "$t:admin.settings.general.asset_url_mode_detect_extension" - }, - { - "id": "asset_url_mode_msg_extensionless", - "type": "basic", - "name": "P", - "if": "{{_local.asset_url_mode_detect_status === 'extensionless'}}", - "props": { - "className": "mt-2 text-sm text-amber-600 dark:text-amber-400" - }, - "text": "$t:admin.settings.general.asset_url_mode_detect_extensionless" - }, - { - "id": "asset_url_mode_msg_unavailable", - "type": "basic", - "name": "P", - "if": "{{_local.asset_url_mode_detect_status === 'unavailable'}}", - "props": { - "className": "mt-2 text-sm text-red-600 dark:text-red-400" - }, - "text": "$t:admin.settings.general.asset_url_mode_detect_unavailable" + "event": "onClick", + "handler": "detectAssetUrlMode" } ] } ] + }, + { + "id": "asset_url_mode_msg_detecting", + "type": "basic", + "name": "P", + "if": "{{_local.asset_url_mode_detect_status === 'detecting'}}", + "props": { + "className": "mt-2 text-sm text-gray-500 dark:text-gray-400" + }, + "text": "$t:admin.settings.general.asset_url_mode_detecting" + }, + { + "id": "asset_url_mode_msg_extension", + "type": "basic", + "name": "P", + "if": "{{_local.asset_url_mode_detect_status === 'extension'}}", + "props": { + "className": "mt-2 text-sm text-green-600 dark:text-green-400" + }, + "text": "$t:admin.settings.general.asset_url_mode_detect_extension" + }, + { + "id": "asset_url_mode_msg_extensionless", + "type": "basic", + "name": "P", + "if": "{{_local.asset_url_mode_detect_status === 'extensionless'}}", + "props": { + "className": "mt-2 text-sm text-amber-600 dark:text-amber-400" + }, + "text": "$t:admin.settings.general.asset_url_mode_detect_extensionless" + }, + { + "id": "asset_url_mode_msg_unavailable", + "type": "basic", + "name": "P", + "if": "{{_local.asset_url_mode_detect_status === 'unavailable'}}", + "props": { + "className": "mt-2 text-sm text-red-600 dark:text-red-400" + }, + "text": "$t:admin.settings.general.asset_url_mode_detect_unavailable" } ] } diff --git a/tests/Playwright/specs/admin/settings-asset-url-mode-detect.spec.ts b/tests/Playwright/specs/admin/settings-asset-url-mode-detect.spec.ts index 563d4fe4..84e4950e 100644 --- a/tests/Playwright/specs/admin/settings-asset-url-mode-detect.spec.ts +++ b/tests/Playwright/specs/admin/settings-asset-url-mode-detect.spec.ts @@ -41,6 +41,12 @@ test('#486 - 자동 감지 버튼이 관리자 다른 버튼과 동일한 btn const button = page.locator(DETECT_BUTTON); await expect(button).toBeAttached(); + // 에셋 서빙 방식은 언어/시간대와 무관한 인프라 설정이므로 지역화 카드가 아니라 + // 전용 카드(#card_asset_serving)에 있어야 한다 — 배치 회귀 가드. + await expect(page.locator('#card_asset_serving')).toBeAttached(); + await expect(page.locator(`#card_asset_serving ${FIELD}`)).toBeAttached(); + await expect(page.locator(`#card_localization ${FIELD}`)).toHaveCount(0); + // btn 베이스 클래스 누락이 이 이슈의 원인이었다 (평문처럼 보임) — 클래스 회귀 가드. await expect(button).toHaveClass(/\bbtn\b/); await expect(button).toHaveClass(/btn-secondary/);