refactor(admin-template): 에셋 파일 서빙 방식을 지역화에서 전용 카드로 분리

에셋 파일 서빙 방식은 언어·시간대와 무관한 인프라(서버 라우팅) 설정이라
지역화 카드에 묶이는 것이 의미상 어긋난다. tab_content_general 하위에
전용 카드(card_asset_serving)를 신설해 필드·자동 감지 버튼·인라인 안내를
이동하고, 라벨/설명을 카드 헤더로 승격해 다른 카드와 형태를 맞췄다.

배치 회귀 가드: 레이아웃 구조 테스트 + E2E 로 전용 카드에 항목이 있고
지역화 카드에는 없음을 단언.
This commit is contained in:
HeuJung
2026-07-23 21:13:39 +09:00
parent 05e81c6017
commit 65f1c70ffe
3 changed files with 168 additions and 120 deletions
@@ -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 의
@@ -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"
}
]
}
@@ -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/);