refactor(admin-template): 에셋 파일 서빙 방식을 지역화에서 전용 카드로 분리
에셋 파일 서빙 방식은 언어·시간대와 무관한 인프라(서버 라우팅) 설정이라 지역화 카드에 묶이는 것이 의미상 어긋난다. tab_content_general 하위에 전용 카드(card_asset_serving)를 신설해 필드·자동 감지 버튼·인라인 안내를 이동하고, 라벨/설명을 카드 헤더로 승격해 다른 카드와 형태를 맞췄다. 배치 회귀 가드: 레이아웃 구조 테스트 + E2E 로 전용 카드에 항목이 있고 지역화 카드에는 없음을 단언.
This commit is contained in:
+38
@@ -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 의
|
||||
|
||||
+124
-120
@@ -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/);
|
||||
|
||||
Reference in New Issue
Block a user