fix(sirsoft-basic, sirsoft-admin_basic): 모바일 드로어 선호설정 아코디언화 및 뷰포트 개선
모바일 헤더 우측 그룹이 420px 를 요구해 폰 뷰포트에서 통화 선택기가 잘렸다. 언어·통화·배송국가를 드로어로 옮겨 가로 칩으로 통일했으나, 이번엔 드로어가 세로로 길어져 정작 메뉴가 스크롤 밖으로 밀렸다. 언어와 통화·배송국가를 각각 독립 아코디언(기본 접힘)으로 바꾸고, 접힘 상태에서도 트리거에 현재 선택값을 요약 표기해 펼치지 않고 확인할 수 있게 했다. 언어는 템플릿이, 통화·배송국가는 이커머스 주입 조각이 각자 상태를 소유해 서로 간섭하지 않는다. 통화 패널은 새 플래그를 만들지 않고 데스크톱 드롭다운의 기존 토글을 재사용한다 — portable 오버라이드에서 if 를 생략하면 엔진이 base if 를 상속하므로 자동으로 기본 접힘이 된다. 이 과정에서 모바일로 펼칠 때 전체 화면 배경 오버레이가 드로어 클릭을 가로채는 결함을 함께 차단했다. 그 밖에 모달 좌우 여백, 주문 취소·일괄변경 팝업의 세로 스택 전환, 은행 관리 입력칸 축소, 쇼핑몰 환경설정·쿠폰·엑셀 다운로드 화면의 가로 넘침을 함께 해소했다.
This commit is contained in:
1 parent
e3c00addd8
commit
0ff4b674ac
49 files changed
+2048
-609
No files matched your search
@@ -8,7 +8,16 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- 휴대폰·태블릿 화면에서 통화·배송국가 선택이 드롭다운 대신 항상 펼쳐진 가로 버튼으로 표시됩니다. 좁은 화면의 메뉴 안에서 드롭다운이 잘려 보이던 문제가 함께 해소됩니다. 데스크톱 화면의 드롭다운은 그대로입니다.
|
||||
- 휴대폰·태블릿 화면에서 통화·배송국가 선택이 메뉴 안의 접었다 펼치는 항목으로 바뀌었습니다. 접혀 있을 때도 현재 선택된 통화와 배송국가가 함께 표시되어 펼치지 않아도 확인할 수 있고, 펼치면 가로 버튼으로 나열됩니다. 좁은 화면의 메뉴 안에서 드롭다운이 잘려 보이던 문제가 함께 해소됩니다. 데스크톱 화면의 드롭다운은 그대로입니다. 관리자 화면도 동일하게 적용됩니다.
|
||||
- 휴대폰 화면에서 주문 취소·주문상태 일괄변경 팝업의 상품 목록이 표 대신 상품별 카드 형태로 표시됩니다.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 쇼핑몰 환경설정의 은행 관리에서 은행코드를 입력해도 입력한 값이 보이지 않던 문제를 수정했습니다 — 좁은 화면에서 입력칸이 글자가 들어갈 수 없을 만큼 줄어들고 있었습니다.
|
||||
- 휴대폰 화면에서 주문 취소·주문상태 일괄변경 팝업의 금액이 "249,000원" 대신 "249,000" / "원" 처럼 숫자 중간에서 잘려 보이던 문제를 수정했습니다. 금액은 앞으로도 숫자 중간에서 줄바꿈되지 않으며, "결제완료" 같은 상태 이름도 "결제완" / "료" 로 쪼개지지 않습니다.
|
||||
- 휴대폰 화면에서 쇼핑몰 환경설정 화면이 좌우로 넘쳐 가로 스크롤이 생기던 문제를 수정했습니다 (이메일 입력 줄, 쇼핑몰 이름·종목 입력칸, 탭 영역).
|
||||
- 휴대폰 화면에서 쿠폰 발급기간 입력칸과 엑셀 다운로드 기간 검색 입력칸이 화면 밖으로 넘치던 문제를 수정했습니다.
|
||||
- 은행 관리·도서산간 배송비 팝업의 표가 좁은 화면에서 가로로 스크롤되도록 명시해, 앞으로 표가 잘려 보이지 않도록 했습니다.
|
||||
|
||||
## [1.0.1] - 2026-07-08
|
||||
|
||||
|
||||
+125
-3
@@ -44,8 +44,15 @@
|
||||
"name": "Div",
|
||||
"slot": "header_currency",
|
||||
"props": { "className": "relative {{(_global.availableCurrencies ?? []).length > 0 || ((_global.availableShippingCountries ?? []).length > 1 && _global.modules?.['sirsoft-ecommerce']?.shipping?.international_shipping_enabled === true) ? '' : 'hidden'}}" },
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"comment": "portable 에서는 이 조각이 관리자 템플릿 모바일 드로어(admin_drawer_prefs, 세로 블록) 안에 렌더된다. 펼친 목록이 컨테이너 폭을 채우도록 w-full. hidden 게이트 표현식은 그대로 유지(props 얕은 머지가 아니라 className 통째 교체이므로 표현식을 복제해야 함).",
|
||||
"props": { "className": "relative w-full {{(_global.availableCurrencies ?? []).length > 0 || ((_global.availableShippingCountries ?? []).length > 1 && _global.modules?.['sirsoft-ecommerce']?.shipping?.international_shipping_enabled === true) ? '' : 'hidden'}}" }
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"comment": "트리거 버튼 — 데스크톱은 absolute 드롭다운의 토글, portable(드로어 안)에서는 접기/펼치기 아코디언의 트리거. 두 경우 모두 같은 플래그(_local.showCurrencyDropdown)를 토글하므로 기본은 접힘이다. portable 에서는 드로어 폭을 꽉 채우는 행 형태(좌: '통화' 라벨, 우: 현재 통화·배송국가 요약 + chevron)로 바뀐다.",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
@@ -54,6 +61,55 @@
|
||||
"aria-expanded": "{{_local.showCurrencyDropdown ?? false}}",
|
||||
"aria-label": "$t:sirsoft-ecommerce.common.currency_label"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"comment": "드로어 안에서는 언어 아코디언 트리거와 동일한 형태 — children 을 통째 교체해 '통화' 라벨 + 현재값 요약 + chevron 으로 구성한다(엔진은 responsive.children 을 완전 교체한다).",
|
||||
"props": {
|
||||
"className": "w-full flex items-center justify-between py-1 cursor-pointer",
|
||||
"aria-haspopup": "listbox",
|
||||
"aria-expanded": "{{_local.showCurrencyDropdown ?? false}}",
|
||||
"aria-label": "$t:sirsoft-ecommerce.common.currency_label"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": { "className": "text-xs font-medium text-gray-500 dark:text-gray-400 uppercase" },
|
||||
"text": "$t:sirsoft-ecommerce.common.currency_label"
|
||||
},
|
||||
{
|
||||
"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.preferredCurrency ?? _global.modules?.['sirsoft-ecommerce']?.language_currency?.default_currency ?? 'KRW'}}"
|
||||
},
|
||||
{
|
||||
"comment": "접힘 상태 요약 ② 현재 배송국가명 (해외배송 활성 + 국가 2개 이상일 때만).",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{(_global.availableShippingCountries ?? []).length > 1 && _global.modules?.['sirsoft-ecommerce']?.shipping?.international_shipping_enabled === true}}",
|
||||
"props": { "className": "text-gray-500 dark:text-gray-400" },
|
||||
"text": "· {{(_global.availableShippingCountries ?? []).find(c => c.code === (_global.preferredShippingCountry ?? _global.modules?.['sirsoft-ecommerce']?.shipping?.default_country ?? 'KR'))?.name ?? (_global.preferredShippingCountry ?? 'KR')}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "chevron-down",
|
||||
"className": "{{_local.showCurrencyDropdown ? 'rotate-180' : ''}} text-xs text-gray-400 dark:text-gray-500 transition-transform duration-200"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"comment": "트리거 배송국가 코드 (해외배송 활성 시) — 관리자 헤더는 flag 없이 코드 병기",
|
||||
@@ -76,7 +132,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "드롭다운 메뉴",
|
||||
"comment": "드롭다운 메뉴 (통화 + 배송국가 2섹션). portable(0~1023px) 에서는 absolute 팝오버가 드로어 스크롤 경계에서 잘리므로 static 인라인 목록으로 전환한다. 표시 여부는 데스크톱과 동일하게 _local.showCurrencyDropdown 이 결정한다(responsive 에 if 를 두지 않으면 base if 를 그대로 상속) → 기본 접힘.",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.showCurrencyDropdown}}",
|
||||
@@ -84,13 +140,34 @@
|
||||
"className": "absolute right-0 top-full mt-2 w-44 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50 py-1 overflow-hidden",
|
||||
"role": "listbox"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"comment": "드로어 안에서는 패널이 곧 본문이므로 카드 크롬(테두리/배경/좌우 패딩)을 벗긴다. 크롬을 남기면 드로어 패딩 안에 패딩이 중첩되어 칩 가용폭이 199px 로 줄고, 같은 드로어의 언어 칩(224px)과 줄바꿈 위치가 어긋난다.",
|
||||
"props": {
|
||||
"className": "static w-full mt-2 bg-transparent border-0 shadow-none py-0",
|
||||
"role": "listbox"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"comment": "섹션 라벨 '통화'. portable 에서는 아코디언 트리거 행이 이미 '통화' 라벨을 달고 있어 중복이므로 숨긴다(배송국가 라벨은 두 번째 섹션을 구분해야 하므로 유지).",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": { "className": "px-4 py-1.5 text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wide" },
|
||||
"responsive": { "portable": { "if": "{{false}}" } },
|
||||
"text": "$t:sirsoft-ecommerce.common.currency_label"
|
||||
},
|
||||
{
|
||||
"comment": "옵션 래퍼. portable 에서만 가로 칩 나열(flex-wrap). 데스크톱은 무클래스 블록이라 기존 세로 목록 그대로. iteration 은 아래 Div 자신에 걸려 있어 항목 수만큼 복제되므로, 복제본들이 이 래퍼의 flex item 이 된다.",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": { "className": "flex flex-wrap gap-2 pb-1" }
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
@@ -104,21 +181,33 @@
|
||||
"role": "option",
|
||||
"aria-selected": "{{_global.preferredCurrency === currency.code}}"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm border rounded-full cursor-pointer whitespace-nowrap transition-colors {{_global.preferredCurrency === currency.code ? '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'}}",
|
||||
"role": "option",
|
||||
"aria-selected": "{{_global.preferredCurrency === currency.code}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Span", "props": { "className": "text-base" }, "text": "{{currency.flag}}" },
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": { "className": "flex-1" },
|
||||
"responsive": { "portable": { "props": { "className": "" } } },
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Span", "props": { "className": "font-medium" }, "text": "{{currency.code}}" },
|
||||
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-400 ml-1" }, "text": "({{currency.symbol}})" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "portable(칩 나열)에서는 선택 상태가 배경/테두리 색으로 이미 표현되고 aria-selected 로도 전달되므로, 칩 폭을 아끼려 체크 아이콘을 숨긴다.",
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{_global.preferredCurrency === currency.code}}",
|
||||
"responsive": { "portable": { "if": "{{false}}" } },
|
||||
"props": { "name": "check", "className": "text-base" }
|
||||
}
|
||||
],
|
||||
@@ -135,6 +224,8 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "── 구분선 ──",
|
||||
@@ -153,8 +244,19 @@
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": { "className": "px-4 py-1.5 text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wide" },
|
||||
"responsive": { "portable": { "props": { "className": "py-1 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase" } } },
|
||||
"text": "$t:sirsoft-ecommerce.common.shipping_country_label"
|
||||
},
|
||||
{
|
||||
"comment": "옵션 래퍼. portable 에서만 가로 칩 나열(flex-wrap). 데스크톱은 무클래스 블록.",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": { "className": "flex flex-wrap gap-2 pb-1" }
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
@@ -168,12 +270,29 @@
|
||||
"role": "option",
|
||||
"aria-selected": "{{(_global.preferredShippingCountry ?? 'KR') === shipCountry.code}}"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm border rounded-full cursor-pointer whitespace-nowrap transition-colors {{(_global.preferredShippingCountry ?? 'KR') === shipCountry.code ? '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'}}",
|
||||
"role": "option",
|
||||
"aria-selected": "{{(_global.preferredShippingCountry ?? 'KR') === shipCountry.code}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{ "type": "basic", "name": "Span", "props": { "className": "flex-1 font-medium" }, "text": "{{shipCountry.name ?? shipCountry.code}}" },
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": { "className": "flex-1 font-medium" },
|
||||
"responsive": { "portable": { "props": { "className": "font-medium" } } },
|
||||
"text": "{{shipCountry.name ?? shipCountry.code}}"
|
||||
},
|
||||
{
|
||||
"comment": "portable(칩 나열)에서는 선택 상태를 배경/테두리 색 + aria-selected 로 전달하므로 체크 아이콘을 숨긴다.",
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{(_global.preferredShippingCountry ?? 'KR') === shipCountry.code}}",
|
||||
"responsive": { "portable": { "if": "{{false}}" } },
|
||||
"props": { "name": "check", "className": "text-base" }
|
||||
}
|
||||
],
|
||||
@@ -191,15 +310,18 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "배경 클릭 시 닫기",
|
||||
"comment": "배경 클릭 시 닫기 — 데스크톱 드롭다운 전용. portable 은 항상 펼친 칩 목록이라 백드롭이 불필요(있으면 화면 전체를 덮어 클릭을 가로챈다).",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.showCurrencyDropdown}}",
|
||||
"responsive": { "portable": { "if": "{{false}}" } },
|
||||
"props": { "className": "fixed inset-0 z-40" },
|
||||
"actions": [
|
||||
{
|
||||
|
||||
+51
-6
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"comment": "트리거 버튼 — 데스크톱 전용. 모바일/태블릿(portable)에서는 이 조각이 드로어(overflow-y-auto) 안에 렌더되어 absolute 드롭다운이 잘리므로, 트리거를 숨기고 패널을 항상 펼친다.",
|
||||
"comment": "트리거 버튼 — 데스크톱은 absolute 드롭다운의 토글, portable(드로어 안)에서는 접기/펼치기 아코디언의 트리거. 두 경우 모두 같은 플래그(_local.showCurrencyDropdown)를 토글하므로 기본은 접힘이다. portable 에서는 드로어 폭을 꽉 채우는 행 형태(좌: '통화' 라벨, 우: 현재 통화·배송국가 요약 + chevron)로 바뀐다.",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
@@ -31,7 +31,51 @@
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": { "className": "hidden" }
|
||||
"comment": "드로어 안에서는 언어 아코디언 트리거와 동일한 형태 — children 을 통째 교체해 '통화' 라벨 + 현재값 요약 + chevron 으로 구성한다(엔진은 responsive.children 을 완전 교체한다).",
|
||||
"props": {
|
||||
"className": "w-full flex items-center justify-between py-1 cursor-pointer",
|
||||
"aria-haspopup": "listbox",
|
||||
"aria-expanded": "{{_local.showCurrencyDropdown ?? false}}",
|
||||
"aria-label": "$t:sirsoft-ecommerce.common.currency_label"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": { "className": "text-xs font-medium text-gray-500 dark:text-gray-400 uppercase" },
|
||||
"text": "$t:sirsoft-ecommerce.common.currency_label"
|
||||
},
|
||||
{
|
||||
"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.preferredCurrency ?? _global.defaultCurrency ?? 'KRW'}}"
|
||||
},
|
||||
{
|
||||
"comment": "접힘 상태 요약 ② 현재 배송국가명 (해외배송 활성 + 국가 2개 이상일 때만). 통화와 시각적으로 구분되도록 옅은 색.",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{(_global.availableShippingCountries ?? []).length > 1 && _global.modules?.['sirsoft-ecommerce']?.shipping?.international_shipping_enabled === true}}",
|
||||
"props": { "className": "text-gray-500 dark:text-gray-400" },
|
||||
"text": "· {{(_global.availableShippingCountries ?? []).find(c => c.code === (_global.preferredShippingCountry ?? _global.modules?.['sirsoft-ecommerce']?.shipping?.default_country ?? 'KR'))?.name ?? (_global.preferredShippingCountry ?? 'KR')}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "chevron-down",
|
||||
"className": "{{_local.showCurrencyDropdown ? 'rotate-180' : ''}} text-xs text-gray-400 dark:text-gray-500 transition-transform duration-200"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
@@ -56,7 +100,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "드롭다운 메뉴 (통화 + 배송국가 2섹션). portable(0~1023px) 에서는 트리거 없이 항상 펼쳐진 정적 목록 — 드로어 스크롤 경계에서 absolute 패널이 잘리는 문제 회피.",
|
||||
"comment": "드롭다운 메뉴 (통화 + 배송국가 2섹션). portable(0~1023px) 에서는 absolute 팝오버가 드로어 스크롤 경계에서 잘리므로 static 인라인 목록으로 전환한다. 표시 여부는 데스크톱과 동일하게 _local.showCurrencyDropdown 이 결정한다(responsive 에 if 를 두지 않으면 base if 를 그대로 상속) → 기본 접힘.",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.showCurrencyDropdown}}",
|
||||
@@ -66,7 +110,6 @@
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"if": "{{true}}",
|
||||
"props": {
|
||||
"className": "static w-full mt-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg py-1",
|
||||
"role": "listbox"
|
||||
@@ -81,10 +124,11 @@
|
||||
"if": "{{(_global.availableCurrencies ?? []).length > 0}}",
|
||||
"children": [
|
||||
{
|
||||
"comment": "섹션 라벨 '통화'. portable 에서는 아코디언 트리거 행이 이미 '통화' 라벨을 달고 있어 중복이므로 숨긴다(배송국가 라벨은 두 번째 섹션을 구분해야 하므로 유지).",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": { "className": "px-4 py-1.5 text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wide" },
|
||||
"responsive": { "portable": { "props": { "className": "px-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase" } } },
|
||||
"responsive": { "portable": { "if": "{{false}}" } },
|
||||
"text": "$t:sirsoft-ecommerce.common.currency_label"
|
||||
},
|
||||
{
|
||||
@@ -298,10 +342,11 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "배경 클릭 시 닫기",
|
||||
"comment": "배경 클릭 시 닫기 — 데스크톱 팝오버 전용. portable 에서는 패널이 드로어 안 인라인 아코디언이라 배경 개념이 없다. 여기서 if 를 끄지 않으면 모바일에서 펼치는 순간 fixed inset-0 오버레이가 드로어 전체를 덮어 모든 클릭을 가로챈다.",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.showCurrencyDropdown}}",
|
||||
"responsive": { "portable": { "if": "{{false}}" } },
|
||||
"props": { "className": "fixed inset-0 z-40" },
|
||||
"actions": [
|
||||
{
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @file adminHeaderCurrencySelectorPortable.test.tsx
|
||||
* @description 관리자 헤더 통화/배송국가 주입 조각의 portable(모바일·태블릿) 칩 전환 회귀 테스트
|
||||
*
|
||||
* 배경: 관리자 모바일 헤더 우측 그룹이 420px 를 요구해 폰 뷰포트에서 통화 셀렉터가 잘렸다.
|
||||
* 템플릿이 통화 슬롯을 모바일 드로어(`admin_drawer_prefs`)로 옮겼고, 이 조각은 드로어 안에서
|
||||
* 접기/펼치기 아코디언(트리거 행 + static 가로 칩 목록)으로 렌더된다(사용자 조각
|
||||
* `header-currency-selector-user` 와 동일 사상).
|
||||
*
|
||||
* 접힘이 기본이며, 표시 여부는 데스크톱과 같은 `_local.showCurrencyDropdown` 이 결정한다 —
|
||||
* portable 오버라이드에 `if` 를 두지 않으면 엔진이 base `if` 를 상속하기 때문(`override.if ?? baseDef.if`).
|
||||
* 데스크톱(≥1024px)은 기존 absolute 드롭다운을 그대로 유지해야 한다.
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const fragmentPath = path.resolve(
|
||||
__dirname,
|
||||
'../../../extensions/header-currency-selector-admin.json',
|
||||
);
|
||||
|
||||
const fragment = JSON.parse(fs.readFileSync(fragmentPath, 'utf8'));
|
||||
const root = fragment.injections[0].components[0];
|
||||
const [trigger, panel, backdrop] = root.children;
|
||||
|
||||
describe('관리자 통화 주입 조각 — portable 칩 전환', () => {
|
||||
it('데스크톱 기본 경로는 absolute 드롭다운을 유지한다 (회귀 방지)', () => {
|
||||
expect(root.props.className).toContain('relative');
|
||||
expect(root.props.className).not.toContain('w-full');
|
||||
|
||||
expect(panel.props.className).toContain('absolute');
|
||||
expect(panel.props.className).toContain('z-50');
|
||||
expect(panel.props.className).toContain('w-44');
|
||||
expect(panel.if).toBe('{{_local.showCurrencyDropdown}}');
|
||||
});
|
||||
|
||||
it('portable 에서 루트가 w-full 이 되고 hidden 게이트 표현식이 보존된다', () => {
|
||||
const cls = root.responsive.portable.props.className;
|
||||
expect(cls).toContain('w-full');
|
||||
// props 는 얕은 머지가 아니라 className 통째 교체이므로 표현식을 복제해야 한다.
|
||||
expect(cls).toContain('availableCurrencies');
|
||||
expect(cls).toContain('international_shipping_enabled');
|
||||
});
|
||||
|
||||
it('portable 트리거는 아코디언 행이 되고 접힘 상태 요약(통화·배송국가)을 노출한다', () => {
|
||||
expect(trigger.name).toBe('Button');
|
||||
|
||||
const t = trigger.responsive.portable;
|
||||
// 더 이상 숨기지 않는다 — 드로어 폭을 채우는 트리거 행
|
||||
expect(t.props.className).not.toBe('hidden');
|
||||
expect(t.props.className).toContain('w-full');
|
||||
expect(t.props.className).toContain('justify-between');
|
||||
// 얕은 머지가 아니라 통째 교체이므로 aria-* 를 오버라이드에도 명시
|
||||
expect(t.props['aria-expanded']).toBe('{{_local.showCurrencyDropdown ?? false}}');
|
||||
|
||||
// 접힘 상태에서 현재 값을 알 수 있어야 한다
|
||||
const summary = JSON.stringify(t.children);
|
||||
expect(summary).toContain('preferredCurrency');
|
||||
expect(summary).toContain('preferredShippingCountry');
|
||||
expect(summary).toContain('chevron-down');
|
||||
expect(summary).toContain("{{_local.showCurrencyDropdown ? 'rotate-180' : ''}}");
|
||||
});
|
||||
|
||||
it('portable 패널도 _local.showCurrencyDropdown 으로 게이트된다 (기본 접힘)', () => {
|
||||
const p = panel.responsive.portable;
|
||||
// 핵심: `{{true}}` 로 강제하면 드로어에서 항상 펼쳐져 아코디언이 성립하지 않는다.
|
||||
// if 를 생략해야 base if 를 상속한다.
|
||||
expect(p.if).toBeUndefined();
|
||||
expect(p.props.className).toContain('static');
|
||||
expect(p.props.className).not.toContain('absolute');
|
||||
expect(p.props.className).not.toContain('z-50');
|
||||
// 얕은 머지라 role 을 재선언하지 않으면 유실된다.
|
||||
expect(p.props.role).toBe('listbox');
|
||||
});
|
||||
|
||||
it('portable 백드롭은 렌더되지 않는다 (fixed inset-0 이 드로어 전체 클릭을 가로챈다)', () => {
|
||||
expect(backdrop.props.className).toContain('fixed');
|
||||
expect(backdrop.responsive.portable.if).toBe('{{false}}');
|
||||
});
|
||||
|
||||
it('칩 래퍼는 정적이고 iteration 은 그 자식에 걸린다', () => {
|
||||
// 엔진은 iteration 을 가진 노드를 항목 수만큼 복제한다. flex 컨테이너에 걸면
|
||||
// 컨테이너가 복제되어 칩이 세로로 쌓인다.
|
||||
const wrappers = panel.children.filter(
|
||||
(c: any) => c.responsive?.portable?.props?.className?.includes('flex-wrap'),
|
||||
);
|
||||
// 통화 섹션 래퍼 1 + 배송국가 섹션 래퍼 1
|
||||
const shippingSection = panel.children.find(
|
||||
(c: any) => c.comment?.includes('배송국가 섹션'),
|
||||
);
|
||||
const shippingWrapper = shippingSection.children.find(
|
||||
(c: any) => c.responsive?.portable?.props?.className?.includes('flex-wrap'),
|
||||
);
|
||||
|
||||
expect(wrappers.length).toBe(1); // 통화 섹션은 panel 직계
|
||||
expect(shippingWrapper).toBeTruthy();
|
||||
|
||||
for (const wrapper of [wrappers[0], shippingWrapper]) {
|
||||
expect(wrapper.iteration).toBeUndefined();
|
||||
const iterating = wrapper.children[0];
|
||||
expect(iterating.iteration).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('portable 옵션은 rounded-full 칩이고 체크 아이콘은 숨는다', () => {
|
||||
const currencyWrapper = panel.children.find(
|
||||
(c: any) => c.responsive?.portable?.props?.className?.includes('flex-wrap'),
|
||||
);
|
||||
const button = currencyWrapper.children[0].children[0];
|
||||
|
||||
const chipCls = button.responsive.portable.props.className;
|
||||
expect(chipCls).toContain('rounded-full');
|
||||
expect(chipCls).toContain('inline-flex');
|
||||
expect(chipCls).toContain('whitespace-nowrap');
|
||||
// 얕은 머지 — role/aria-selected 재선언 필수
|
||||
expect(button.responsive.portable.props.role).toBe('option');
|
||||
expect(button.responsive.portable.props['aria-selected']).toBeTruthy();
|
||||
|
||||
const check = button.children.find((c: any) => c.name === 'Icon');
|
||||
expect(check.responsive.portable.if).toBe('{{false}}');
|
||||
});
|
||||
|
||||
it('portable 패널은 드로어 안에서 카드 크롬을 벗는다 (언어 칩과 줄바꿈 정렬)', () => {
|
||||
const cls = panel.responsive.portable.props.className;
|
||||
// 크롬을 남기면 드로어 패딩 안에 패딩이 중첩되어 칩 가용폭이 줄고 언어 칩과 어긋난다.
|
||||
expect(cls).toContain('bg-transparent');
|
||||
expect(cls).toContain('border-0');
|
||||
expect(cls).toContain('shadow-none');
|
||||
});
|
||||
});
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* @file adminMobileViewportOverflow.test.tsx
|
||||
* @description 관리자 모바일 뷰포트 넘침/압착 회귀 테스트 (이커머스 소유분)
|
||||
*
|
||||
* 각 단언은 브라우저 실측(390px / 320px)으로 확인된 결함에 1:1 대응한다.
|
||||
* 되돌리면 해당 결함이 그대로 재발하므로, 사유를 각 테스트에 남긴다.
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const layoutsDir = path.resolve(__dirname, '../../../layouts/admin');
|
||||
|
||||
const load = (rel: string): any =>
|
||||
JSON.parse(fs.readFileSync(path.resolve(layoutsDir, rel), 'utf8'));
|
||||
|
||||
/** 트리 전체를 순회하며 술어를 만족하는 노드를 모은다. */
|
||||
function collect(node: any, pred: (n: any) => boolean, acc: any[] = []): any[] {
|
||||
if (!node || typeof node !== 'object') return acc;
|
||||
if (Array.isArray(node)) {
|
||||
for (const n of node) collect(n, pred, acc);
|
||||
return acc;
|
||||
}
|
||||
if (pred(node)) acc.push(node);
|
||||
for (const v of Object.values(node)) {
|
||||
if (v && typeof v === 'object') collect(v, pred, acc);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
const cls = (n: any): string => {
|
||||
const c = n?.props?.className;
|
||||
return typeof c === 'string' ? c : '';
|
||||
};
|
||||
|
||||
describe('B1 — 모달 좌우 여백 (max-w-full 트랩)', () => {
|
||||
// .modal-container 의 max-w-[calc(100vw-2rem)] 와 .max-w-full 은 명시도가 같고(0,1,0)
|
||||
// 빌드 CSS 에서 .max-w-full 이 뒤에 정의되므로 승리한다. 레이아웃에서 max-w-full 을
|
||||
// 주면 clamp 가 무효화되어 모달이 화면 좌우에 붙는다(여백 0).
|
||||
it.each([
|
||||
'partials/admin_ecommerce_order_detail/_modal_cancel_order.json',
|
||||
'partials/admin_ecommerce_order_detail/_modal_batch_change_confirm.json',
|
||||
])('%s 는 max-w-full 을 갖지 않는다', (rel) => {
|
||||
const raw = fs.readFileSync(path.resolve(layoutsDir, rel), 'utf8');
|
||||
expect(raw).not.toContain('max-w-full');
|
||||
});
|
||||
});
|
||||
|
||||
describe('B2 — 주문상세 모달 상품표 카드형 전환', () => {
|
||||
it.each([
|
||||
['_modal_cancel_order.json', 'partials/admin_ecommerce_order_detail/_modal_cancel_order.json'],
|
||||
['_modal_batch_change_confirm.json', 'partials/admin_ecommerce_order_detail/_modal_batch_change_confirm.json'],
|
||||
])('%s: grid-cols-12 행이 portable 에서 1열로 스택된다', (_n, rel) => {
|
||||
const layout = load(rel);
|
||||
const grids = collect(layout, (n) => cls(n).includes('grid-cols-12'));
|
||||
|
||||
// 헤더 행 1 + 본문(iteration) 행 1
|
||||
expect(grids.length).toBe(2);
|
||||
|
||||
const header = grids.find((g) => cls(g).includes('bg-gray-50'));
|
||||
const body = grids.find((g) => !cls(g).includes('bg-gray-50'));
|
||||
|
||||
// 카드형에서는 열 라벨 행이 의미 없다 → 미렌더
|
||||
expect(header.responsive?.portable?.if).toBe('{{false}}');
|
||||
|
||||
// 390px 에서 12열이면 1열 = 16.4px → `249,000원`(56px) 같은 토큰이 숫자 내부에서 쪼개진다.
|
||||
expect(body.responsive?.portable?.props?.className).toContain('grid-cols-1');
|
||||
expect(body.responsive?.portable?.props?.className).not.toContain('grid-cols-12');
|
||||
});
|
||||
|
||||
it.each([
|
||||
'partials/admin_ecommerce_order_detail/_modal_cancel_order.json',
|
||||
'partials/admin_ecommerce_order_detail/_modal_batch_change_confirm.json',
|
||||
])('%s: 모든 col-span 셀이 portable 에서 span 을 벗는다', (rel) => {
|
||||
const layout = load(rel);
|
||||
const spanCells = collect(layout, (n) => /col-span-\d+/.test(cls(n)));
|
||||
expect(spanCells.length).toBeGreaterThan(0);
|
||||
|
||||
for (const cell of spanCells) {
|
||||
const portable = cell.responsive?.portable?.props?.className;
|
||||
expect(portable).toBeDefined();
|
||||
// 1열 그리드에 col-span-5 가 남으면 암묵 열이 생겨 셀 폭이 들쭉날쭉해진다.
|
||||
expect(portable).not.toMatch(/col-span-\d+/);
|
||||
}
|
||||
});
|
||||
|
||||
// 세로 스택으로 셀이 전폭(285px)이 되어 현재 데이터로는 절단이 없다(10억대 금액도 88px).
|
||||
// 그러나 셀이 다시 좁아지면 즉시 재발하므로, 방어선을 코드에 선언해 둔다.
|
||||
// 실측(16px 셀로 축소):
|
||||
// - 금액 순수 토큰 + whitespace-nowrap → 1줄 유지, 숫자 내부 미분리
|
||||
// - `소계 249,000원`(한글+금액 혼합) + break-keep → `소계` / `249,000원` 2줄, 숫자 보존
|
||||
// (이 문자열에 nowrap 을 주면 셀을 65px 넘겨 더 나쁘다 — 그래서 break-keep)
|
||||
it('금액(순수 숫자) 노드는 whitespace-nowrap 으로 숫자 내부 줄바꿈을 막는다', () => {
|
||||
const cancel = load('partials/admin_ecommerce_order_detail/_modal_cancel_order.json');
|
||||
const batch = load('partials/admin_ecommerce_order_detail/_modal_batch_change_confirm.json');
|
||||
|
||||
const unitPrice = collect(
|
||||
cancel,
|
||||
(n) => typeof n.text === 'string' && n.text.includes('unit_price') && n.text.includes('원'),
|
||||
)[0];
|
||||
expect(cls(unitPrice)).toContain('whitespace-nowrap');
|
||||
|
||||
const batchAmounts = collect(
|
||||
batch,
|
||||
(n) => typeof n.text === 'string' && /Number\(confirmItem\.(original_price|unit_price)\)/.test(n.text),
|
||||
);
|
||||
expect(batchAmounts.length).toBe(2);
|
||||
for (const node of batchAmounts) {
|
||||
expect(cls(node)).toContain('whitespace-nowrap');
|
||||
}
|
||||
});
|
||||
|
||||
it('한글 라벨 노드는 break-keep 으로 음절 경계 절단을 막는다', () => {
|
||||
const cancel = load('partials/admin_ecommerce_order_detail/_modal_cancel_order.json');
|
||||
const batch = load('partials/admin_ecommerce_order_detail/_modal_batch_change_confirm.json');
|
||||
|
||||
// `소계 249,000원` — 한글 라벨 + 금액 혼합. nowrap 이 아니라 break-keep 이 정답.
|
||||
const subtotal = collect(
|
||||
cancel,
|
||||
(n) => typeof n.text === 'string' && n.text.includes('product_subtotal'),
|
||||
)[0];
|
||||
expect(cls(subtotal)).toContain('break-keep');
|
||||
expect(cls(subtotal)).not.toContain('whitespace-nowrap');
|
||||
|
||||
// 상태 배지 (`결제완료` → `결제완`/`료` 방지)
|
||||
const cancelStatus = collect(
|
||||
cancel,
|
||||
(n) => typeof n.text === 'string' && n.text.includes('option_status_label'),
|
||||
)[0];
|
||||
expect(cls(cancelStatus)).toContain('break-keep');
|
||||
|
||||
const batchStatus = collect(
|
||||
batch,
|
||||
(n) => typeof n.text === 'string' && n.text.includes('order.status.payment_complete'),
|
||||
)[0];
|
||||
expect(cls(batchStatus)).toContain('break-keep');
|
||||
});
|
||||
});
|
||||
|
||||
describe('B3 — 은행 관리 모달', () => {
|
||||
const rel = 'partials/admin_ecommerce_settings/_bank_management_modal.json';
|
||||
|
||||
it('은행코드 입력칸에 min-w-[80px] 하한이 있다', () => {
|
||||
const layout = load(rel);
|
||||
// 은행코드 input 은 code 오류 키를 참조하는 className 바인딩으로 식별한다.
|
||||
const input = collect(
|
||||
layout,
|
||||
(n) => n.name === 'Input' && cls(n).includes("+ '.code'"),
|
||||
)[0];
|
||||
|
||||
expect(input).toBeTruthy();
|
||||
// table-layout:auto + td{min-width:0} 이라 좁은 뷰포트에서 input 이 24px(=좌우 padding)로
|
||||
// 압착되어 텍스트 영역이 0px 이 된다. width 지정은 auto 레이아웃이 무시하므로 min-width 필요.
|
||||
expect(cls(input)).toContain('min-w-[80px]');
|
||||
// 숫자 스케일 min-w-16 은 safelist 에 없어 CSS 가 생성되지 않는다(무효).
|
||||
expect(cls(input)).not.toContain('min-w-16');
|
||||
});
|
||||
|
||||
it('표 래퍼가 overflow-x-auto 를 명시한다', () => {
|
||||
const layout = load(rel);
|
||||
const wrapper = collect(layout, (n) => cls(n).includes('max-h-96'))[0];
|
||||
expect(wrapper).toBeTruthy();
|
||||
// overflow-y 만 선언해도 computed 는 auto 가 되지만 암묵적 의존이다.
|
||||
expect(cls(wrapper)).toContain('overflow-x-auto');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C2 — 쿠폰 발급기간 입력 행', () => {
|
||||
const rel = 'partials/admin_ecommerce_promotion_coupon_form/_partial_issue_settings.json';
|
||||
|
||||
it('portable 에서 행이 줄바꿈하고 datetime 입력이 전폭이 된다', () => {
|
||||
const layout = load(rel);
|
||||
const row = collect(layout, (n) => n.id === 'issue_period_range')[0];
|
||||
expect(row).toBeTruthy();
|
||||
// flex-center 는 nowrap. w-52(208px) 두 개가 좁은 뷰포트를 넘긴다.
|
||||
expect(row.responsive.portable.props.className).toContain('flex-wrap');
|
||||
|
||||
const inputs = collect(row, (n) => n.name === 'Input');
|
||||
expect(inputs.length).toBe(2);
|
||||
for (const i of inputs) {
|
||||
expect(i.responsive.portable.props.className).toContain('w-full');
|
||||
expect(i.responsive.portable.props.className).not.toContain('w-52');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('C3 — 이커머스 환경설정 넘침', () => {
|
||||
it('탭바는 sticky-tab-nav-responsive 를 쓴다 (단계적 패딩과 일치)', () => {
|
||||
const layout = load('admin_ecommerce_settings.json');
|
||||
const nav = collect(layout, (n) => n.id === 'tab_navigation')[0];
|
||||
expect(nav).toBeTruthy();
|
||||
// 콘텐츠 래퍼가 p-4 sm:p-6 lg:p-8 이라 고정 24px 를 가정하는 sticky-tab-nav(-mx-6 px-6)는
|
||||
// 폰에서 좌우 8px 씩 과다 bleed 된다.
|
||||
expect(nav.props.className).toBe('sticky-tab-nav-responsive');
|
||||
});
|
||||
|
||||
it('이메일 행이 portable 에서 줄바꿈한다 (rcaExcess 71px 의 진범)', () => {
|
||||
const layout = load('partials/admin_ecommerce_settings/_tab_basic_info.json');
|
||||
const emailGroup = collect(layout, (n) => n.id === 'field_email')[0];
|
||||
expect(emailGroup).toBeTruthy();
|
||||
const row = collect(emailGroup, (n) => cls(n).startsWith('flex-center'))[0];
|
||||
expect(row.responsive.portable.props.className).toContain('flex-wrap');
|
||||
});
|
||||
|
||||
it('w-80 고정폭 입력은 mobile(0~767px) 에서만 전폭이 된다', () => {
|
||||
const layout = load('partials/admin_ecommerce_settings/_tab_basic_info.json');
|
||||
const w80 = collect(layout, (n) => n.name === 'Input' && cls(n).includes('w-80'));
|
||||
expect(w80.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const overridden = w80.filter((n) => n.responsive);
|
||||
expect(overridden.length).toBe(2); // 쇼핑몰 이름 + 종목
|
||||
|
||||
for (const n of overridden) {
|
||||
// 태블릿(768~1023px)은 w-80 이 넘치지 않으므로 portable 로 잡으면 불필요하게 늘어난다.
|
||||
expect(n.responsive.mobile).toBeDefined();
|
||||
expect(n.responsive.portable).toBeUndefined();
|
||||
expect(n.responsive.mobile.props.className).toContain('w-full');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('C5 — 엑셀 다운로드 날짜 필터 행', () => {
|
||||
it('부모가 flex-wrap 이어도 자식 행에 flex-wrap 이 필요하다', () => {
|
||||
const layout = load('admin_ecommerce_excel_download_index.json');
|
||||
const parent = collect(layout, (n) => n.id === 'date_filter_row')[0];
|
||||
expect(parent).toBeTruthy();
|
||||
expect(cls(parent)).toContain('flex-wrap');
|
||||
|
||||
// 부모만 wrap 이면 nowrap 인 자식 행(입력 2개 × w-36)이 11px 넘친다.
|
||||
const child = (parent.children ?? []).find((c: any) => cls(c).startsWith('flex-center'));
|
||||
expect(child).toBeTruthy();
|
||||
expect(cls(child)).toContain('flex-wrap');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C6 — 도서산간 템플릿 모달 표', () => {
|
||||
it('표 컨테이너가 overflow-x-auto 를 명시한다', () => {
|
||||
const layout = load('partials/admin_ecommerce_shipping_policy_form/_modal_extra_fee_template.json');
|
||||
const wrapper = collect(layout, (n) => n.id === 'template_list')[0];
|
||||
expect(wrapper).toBeTruthy();
|
||||
// 현재는 computed 가 우연히 auto 라 동작하지만, overflow-y-visible 로 바뀌면 즉시 표가 잘린다.
|
||||
expect(cls(wrapper)).toContain('overflow-x-auto');
|
||||
});
|
||||
});
|
||||
+35
-6
@@ -120,19 +120,38 @@ describe('header-currency-selector-user.json — 헤더 공용 셀렉터 2섹션
|
||||
* 사용자 템플릿(sirsoft-basic)은 390px 뷰포트 오버플로(+11px) 해소를 위해 이 슬롯을
|
||||
* 헤더 우측 그룹에서 모바일 드로어(overflow-y-auto + w-80)로 옮겼다. 드로어에서는
|
||||
* absolute 드롭다운 패널이 스크롤 경계에서 잘리므로, 주입 노드 자체가 responsive.portable
|
||||
* (0~1023px) 오버라이드로 "트리거 숨김 + 패널 항상 펼침(static)" 을 선언한다.
|
||||
* (0~1023px) 오버라이드로 static 인라인 목록으로 전환한다.
|
||||
*
|
||||
* 표시 여부는 데스크톱과 동일하게 `_local.showCurrencyDropdown` 이 결정한다 — portable 에
|
||||
* `if` 를 두지 않으면 엔진이 base `if` 를 그대로 상속하므로(`override.if ?? baseDef.if`)
|
||||
* 드로어에서도 기본 접힘이 되고, 트리거가 접기/펼치기 아코디언 역할을 한다.
|
||||
*
|
||||
* SlotContainer 는 주입 자식에게 variant/context prop 을 전달하지 않으므로 컨테이너별로
|
||||
* 다르게 그릴 방법이 이것뿐이다. 데스크톱(≥1024px)은 기존 드롭다운을 그대로 유지한다.
|
||||
*/
|
||||
it('트리거 버튼은 portable(0~1023px)에서 숨겨진다 (모바일 드로어 = 펼친 목록)', () => {
|
||||
it('트리거 버튼은 portable 에서 아코디언 행이 되고 접힘 상태 요약(통화·배송국가)을 노출한다', () => {
|
||||
const trigger = walk(root, (n) => n.name === 'Button' && Array.isArray(n.actions) &&
|
||||
n.actions.some((a: any) => a.handler === 'setState' && a.params?.showCurrencyDropdown !== undefined));
|
||||
expect(trigger).toBeTruthy();
|
||||
expect(trigger.responsive?.portable?.props?.className).toBe('hidden');
|
||||
|
||||
const portable = trigger.responsive?.portable;
|
||||
expect(portable).toBeTruthy();
|
||||
// 더 이상 숨기지 않는다 — 드로어 폭을 채우는 트리거 행
|
||||
expect(portable.props.className).not.toBe('hidden');
|
||||
expect(portable.props.className).toContain('w-full');
|
||||
expect(portable.props.className).toContain('justify-between');
|
||||
// 접근성: 얕은 머지가 아니라 통째 교체이므로 aria-* 를 오버라이드에도 명시
|
||||
expect(portable.props['aria-expanded']).toBe('{{_local.showCurrencyDropdown ?? false}}');
|
||||
|
||||
// 접힘 상태에서 현재 값을 알 수 있어야 한다 (요약 = 통화 + 배송국가)
|
||||
const summary = JSON.stringify(portable.children);
|
||||
expect(summary).toContain('preferredCurrency');
|
||||
expect(summary).toContain('preferredShippingCountry');
|
||||
expect(summary).toContain('chevron-down');
|
||||
expect(summary).toContain("{{_local.showCurrencyDropdown ? 'rotate-180' : ''}}");
|
||||
});
|
||||
|
||||
it('패널은 portable 에서 if 해제 + static 흐름 (데스크톱 absolute/z-50 은 보존)', () => {
|
||||
it('패널은 portable 에서도 _local.showCurrencyDropdown 으로 게이트된다 (기본 접힘)', () => {
|
||||
const panel = (root.children ?? []).find(
|
||||
(n: any) => n.name === 'Div' && n.props?.role === 'listbox'
|
||||
);
|
||||
@@ -143,10 +162,11 @@ describe('header-currency-selector-user.json — 헤더 공용 셀렉터 2섹션
|
||||
expect(panel.props.className).toContain('absolute');
|
||||
expect(panel.props.className).toContain('z-50');
|
||||
|
||||
// portable 오버라이드 — 엔진은 override.if 로 base if 를 완전 교체한다
|
||||
const portable = panel.responsive?.portable;
|
||||
expect(portable).toBeTruthy();
|
||||
expect(portable.if).toBe('{{true}}');
|
||||
// 핵심: portable 에 if 를 두지 않아야 base if 를 상속해 기본 접힘이 된다.
|
||||
// `{{true}}` 로 강제하면 드로어에서 항상 펼쳐져 아코디언이 성립하지 않는다.
|
||||
expect(portable.if).toBeUndefined();
|
||||
expect(portable.props.className).toContain('static');
|
||||
expect(portable.props.className).not.toContain('absolute');
|
||||
expect(portable.props.className).not.toContain('z-50');
|
||||
@@ -154,6 +174,15 @@ describe('header-currency-selector-user.json — 헤더 공용 셀렉터 2섹션
|
||||
expect(portable.props.role).toBe('listbox');
|
||||
});
|
||||
|
||||
it('배경 클릭 오버레이는 portable 에서 꺼진다 (드로어 전체를 덮어 클릭을 가로채는 회귀 방지)', () => {
|
||||
const backdrop = (root.children ?? []).find(
|
||||
(n: any) => n.name === 'Div' && (n.props?.className ?? '').includes('fixed inset-0')
|
||||
);
|
||||
expect(backdrop).toBeTruthy();
|
||||
expect(backdrop.if).toBe('{{_local.showCurrencyDropdown}}');
|
||||
expect(backdrop.responsive?.portable?.if).toBe('{{false}}');
|
||||
});
|
||||
|
||||
/**
|
||||
* 모바일(portable)에서는 통화·배송국가를 세로 목록이 아니라 가로 나열 칩으로 그린다.
|
||||
* 세로 목록은 좁은 드로어에서 세로 공간을 과하게 먹는다.
|
||||
|
||||
+23
-22
@@ -50,7 +50,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "file-spreadsheet",
|
||||
"className": "w-5 h-5 icon-muted"
|
||||
"className": "text-xl icon-muted"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -157,7 +157,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "refresh-cw",
|
||||
"className": "w-4 h-4"
|
||||
"className": "text-base"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -230,7 +230,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "check-circle",
|
||||
"className": "w-3 h-3"
|
||||
"className": "text-xs"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -262,7 +262,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "loader",
|
||||
"className": "w-3 h-3"
|
||||
"className": "text-xs"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -294,7 +294,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "alert-circle",
|
||||
"className": "w-3 h-3"
|
||||
"className": "text-xs"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -326,7 +326,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "file-text",
|
||||
"className": "w-3 h-3"
|
||||
"className": "text-xs"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -439,10 +439,11 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "부모(date_filter_row)는 flex-wrap 이지만 이 행 자신은 nowrap 이라, 시작일~종료일(각 input w-36 = 144px)이 좁은 뷰포트에서 콘텐츠 영역을 11px 넘긴다. 자식에도 flex-wrap 을 준다.",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-center gap-2"
|
||||
"className": "flex-center flex-wrap gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
@@ -457,7 +458,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "calendar",
|
||||
"className": "w-4 h-4 icon-muted"
|
||||
"className": "text-base icon-muted"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -502,7 +503,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "calendar",
|
||||
"className": "w-4 h-4 icon-muted"
|
||||
"className": "text-base icon-muted"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -781,7 +782,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "check-circle",
|
||||
"className": "w-3 h-3"
|
||||
"className": "text-xs"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -994,7 +995,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "check-circle",
|
||||
"className": "w-3 h-3"
|
||||
"className": "text-xs"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1161,7 +1162,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "loader",
|
||||
"className": "w-3 h-3 animate-spin"
|
||||
"className": "text-xs animate-spin"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1299,7 +1300,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "alert-circle",
|
||||
"className": "w-3 h-3"
|
||||
"className": "text-xs"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1437,7 +1438,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "file-text",
|
||||
"className": "w-3 h-3"
|
||||
"className": "text-xs"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1581,7 +1582,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "check-circle",
|
||||
"className": "w-3 h-3"
|
||||
"className": "text-xs"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1789,7 +1790,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "loader",
|
||||
"className": "w-3 h-3 animate-spin"
|
||||
"className": "text-xs animate-spin"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1979,7 +1980,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "alert-circle",
|
||||
"className": "w-3 h-3"
|
||||
"className": "text-xs"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2169,7 +2170,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "file-text",
|
||||
"className": "w-3 h-3"
|
||||
"className": "text-xs"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2379,7 +2380,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "chevrons-left",
|
||||
"className": "w-4 h-4"
|
||||
"className": "text-base"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -2398,7 +2399,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "chevron-left",
|
||||
"className": "w-4 h-4"
|
||||
"className": "text-base"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -2434,7 +2435,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "chevron-right",
|
||||
"className": "w-4 h-4"
|
||||
"className": "text-base"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -2452,7 +2453,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "chevrons-right",
|
||||
"className": "w-4 h-4"
|
||||
"className": "text-base"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+3
-2
@@ -268,6 +268,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "className 은 sticky-tab-nav-responsive 를 쓴다 — 이 화면의 콘텐츠 래퍼가 admin-page-content-responsive(p-4 sm:p-6 lg:p-8) 라 뷰포트마다 좌우 패딩이 다르기 때문. 고정 24px 를 가정하는 sticky-tab-nav(-mx-6 px-6)를 쓰면 폰(p-4=16px)에서 좌우로 8px 씩 과다 bleed 되어 콘텐츠 영역을 넘친다.",
|
||||
"id": "tab_navigation",
|
||||
"type": "composite",
|
||||
"name": "TabNavigation",
|
||||
@@ -316,7 +317,7 @@
|
||||
],
|
||||
"activeTabId": "{{_global.activeEcommerceSettingsTab || query.tab || 'basic_info'}}",
|
||||
"variant": "default",
|
||||
"className": "sticky-tab-nav"
|
||||
"className": "sticky-tab-nav-responsive"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
@@ -380,7 +381,7 @@
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "triangle-exclamation",
|
||||
"className": "w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5"
|
||||
"className": "text-xl text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+83
-15
@@ -8,8 +8,7 @@
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-ecommerce.admin.order.detail.modal.batch_change.title",
|
||||
"width": "750px",
|
||||
"className": "max-w-full"
|
||||
"width": "750px"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
@@ -48,7 +47,14 @@
|
||||
"props": {
|
||||
"className": "col-span-5"
|
||||
},
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.batch_change.product_col"
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.batch_change.product_col",
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
@@ -56,7 +62,14 @@
|
||||
"props": {
|
||||
"className": "col-span-2 text-right"
|
||||
},
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.batch_change.price_col"
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.batch_change.price_col",
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "text-left"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
@@ -64,7 +77,14 @@
|
||||
"props": {
|
||||
"className": "col-span-2 text-center"
|
||||
},
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.batch_change.change_quantity_col"
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.batch_change.change_quantity_col",
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "text-left"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
@@ -72,9 +92,21 @@
|
||||
"props": {
|
||||
"className": "col-span-3 text-center"
|
||||
},
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.batch_change.current_status_col"
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.batch_change.current_status_col",
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "text-left"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"if": "{{false}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
@@ -165,7 +197,14 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "flex-center gap-2 min-w-0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
@@ -175,11 +214,12 @@
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"comment": "whitespace-nowrap — 금액은 순수 숫자 토큰이라 숫자 내부에서 절대 줄바꿈되지 않아야 한다(`1,249,000` → `1,249` / `,000`). portable 세로 스택으로 셀이 전폭이라 현재는 절단이 없으나, 셀이 다시 좁아지면 즉시 재발하므로 의도를 코드에 고정한다.",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{confirmItem.original_price && Number(confirmItem.original_price) !== Number(confirmItem.unit_price)}}",
|
||||
"props": {
|
||||
"className": "text-hint line-through"
|
||||
"className": "text-hint line-through whitespace-nowrap"
|
||||
},
|
||||
"text": "{{Number(confirmItem.original_price).toLocaleString()}}"
|
||||
},
|
||||
@@ -187,11 +227,18 @@
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
"className": "text-sm font-medium text-gray-900 dark:text-gray-100 whitespace-nowrap"
|
||||
},
|
||||
"text": "{{Number(confirmItem.unit_price).toLocaleString()}}"
|
||||
}
|
||||
]
|
||||
],
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "text-left"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
@@ -230,7 +277,14 @@
|
||||
},
|
||||
"text": "/ {{confirmItem.quantity}}"
|
||||
}
|
||||
]
|
||||
],
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "flex justify-start"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
@@ -243,13 +297,27 @@
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "{{({'pending_order':'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300','pending_payment':'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300','payment_complete':'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300','shipping_hold':'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300','preparing':'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300','shipping_ready':'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300','shipping':'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300','delivered':'bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-300','cancelled':'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300','confirmed':'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-300'})[confirmItem.option_status] ?? 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}} inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
||||
"className": "{{({'pending_order':'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300','pending_payment':'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300','payment_complete':'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300','shipping_hold':'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300','preparing':'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300','shipping_ready':'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300','shipping':'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300','delivered':'bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-300','cancelled':'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300','confirmed':'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-300'})[confirmItem.option_status] ?? 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}} inline-flex items-center px-2 py-0.5 rounded text-xs font-medium break-keep"
|
||||
},
|
||||
"text": "{{({'pending_order':'$t:sirsoft-ecommerce.admin.order.status.waiting','pending_payment':'$t:sirsoft-ecommerce.admin.order.status.payment_pending','payment_complete':'$t:sirsoft-ecommerce.admin.order.status.payment_complete','shipping_hold':'$t:sirsoft-ecommerce.admin.order.status.shipping_hold','preparing':'$t:sirsoft-ecommerce.admin.order.status.preparing','shipping_ready':'$t:sirsoft-ecommerce.admin.order.status.shipping_ready','shipping':'$t:sirsoft-ecommerce.admin.order.status.shipping','delivered':'$t:sirsoft-ecommerce.admin.order.status.delivered','cancelled':'$t:sirsoft-ecommerce.admin.order.status.cancelled','confirmed':'$t:sirsoft-ecommerce.admin.order.status.confirmed'})[confirmItem.option_status] ?? confirmItem.option_status}}"
|
||||
}
|
||||
]
|
||||
],
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "flex justify-start"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "grid grid-cols-1 gap-2 px-3 py-2 items-center"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+88
-16
@@ -8,8 +8,7 @@
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-ecommerce.admin.order.detail.modal.cancel.title",
|
||||
"width": "1125px",
|
||||
"className": "max-w-full"
|
||||
"width": "1125px"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
@@ -154,7 +153,14 @@
|
||||
"props": {
|
||||
"className": "col-span-5"
|
||||
},
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.cancel.product_col"
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.cancel.product_col",
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
@@ -162,7 +168,14 @@
|
||||
"props": {
|
||||
"className": "col-span-2 text-right"
|
||||
},
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.cancel.unit_price_col"
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.cancel.unit_price_col",
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "text-left"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
@@ -170,7 +183,14 @@
|
||||
"props": {
|
||||
"className": "col-span-3 text-center"
|
||||
},
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.cancel.cancel_quantity_col"
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.cancel.cancel_quantity_col",
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "text-left"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
@@ -178,9 +198,21 @@
|
||||
"props": {
|
||||
"className": "col-span-2 text-center"
|
||||
},
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.cancel.status_col"
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.cancel.status_col",
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "text-left"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"if": "{{false}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "취소 항목 반복",
|
||||
@@ -238,7 +270,14 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "flex-center gap-2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "단가 + 소계",
|
||||
@@ -249,22 +288,31 @@
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"comment": "whitespace-nowrap — 금액 토큰은 숫자 내부에서 절대 줄바꿈되지 않아야 한다(`249,000원` → `249,000` / `원`). portable 세로 스택으로 셀이 전폭이라 현재는 절단이 없으나, 셀이 다시 좁아지면 즉시 재발하므로 의도를 코드에 고정한다.",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-body"
|
||||
"className": "text-body whitespace-nowrap"
|
||||
},
|
||||
"text": "{{(cancelItem.unit_price ?? 0).toLocaleString()}}원 × {{cancelItem.quantity ?? 1}}"
|
||||
},
|
||||
{
|
||||
"comment": "break-keep + whitespace-nowrap 조합 대신 break-keep 만 — `소계`(한글 라벨) + 금액이 한 문자열이라 nowrap 을 주면 좁은 셀에서 넘친다. break-keep 은 어절 경계에서만 줄바꿈해 `소계` / `249,000원` 으로 끊고 숫자 내부는 보존한다.",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-tertiary"
|
||||
"className": "text-tertiary break-keep"
|
||||
},
|
||||
"text": "$t:sirsoft-ecommerce.admin.order.detail.modal.cancel.product_subtotal {{((cancelItem.unit_price ?? 0) * (cancelItem.quantity ?? 1)).toLocaleString()}}원"
|
||||
}
|
||||
]
|
||||
],
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "text-left"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "취소수량 입력",
|
||||
@@ -297,7 +345,14 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "flex justify-start"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "현재상태 Badge",
|
||||
@@ -310,13 +365,31 @@
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"comment": "break-keep — 한글 상태명(`결제완료`)이 음절 경계에서 `결제완`/`료` 로 쪼개지지 않게 어절 단위 줄바꿈을 강제한다. 현재 Badge 는 inline-flex 라 콘텐츠 폭으로 자라 줄바꿈이 없지만, 배지 폭이 제한되면 즉시 재발한다.",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "break-keep"
|
||||
},
|
||||
"text": "{{cancelItem.option_status_label ?? cancelItem.option_status ?? ''}}"
|
||||
}
|
||||
]
|
||||
],
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "grid grid-cols-1 gap-2 px-3 py-2 border-t border-gray-100 dark:border-gray-700 items-center"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1675,8 +1748,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
}
|
||||
"props": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+26
-1
@@ -194,7 +194,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "기간설정 시: 시작일 ~ 종료일",
|
||||
"comment": "기간설정 시: 시작일 ~ 종료일. flex-center 는 nowrap 이고 datetime-local 입력이 각 208px(w-52) 이라 좁은 뷰포트에서 행이 콘텐츠 영역을 넘친다 → portable 에서 줄바꿈 + 입력 전폭.",
|
||||
"id": "issue_period_range",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
@@ -202,6 +202,13 @@
|
||||
"props": {
|
||||
"className": "flex-center gap-2"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "flex-center flex-wrap gap-2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
@@ -210,6 +217,15 @@
|
||||
"type": "datetime-local",
|
||||
"name": "issue_from",
|
||||
"className": "input w-52"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"type": "datetime-local",
|
||||
"name": "issue_from",
|
||||
"className": "input w-full"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -227,6 +243,15 @@
|
||||
"type": "datetime-local",
|
||||
"name": "issue_to",
|
||||
"className": "input w-52"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"type": "datetime-local",
|
||||
"name": "issue_to",
|
||||
"className": "input w-full"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+4
-3
@@ -119,11 +119,11 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "은행 목록 테이블 (스크롤 영역)",
|
||||
"comment": "은행 목록 테이블 (스크롤 영역). overflow-x-auto 명시 필수 — 좁은 뷰포트에서 표(413px)가 컨테이너(356px)를 넘는다. overflow-y 만 선언해도 CSS Overflow 명세상 다른 축이 visible 로 남을 수 없어 computed 값이 auto 가 되지만, 그 의존은 암묵적이다. 누군가 overflow-y-visible 로 바꾸는 순간 표가 잘리므로 의도를 코드에 고정한다.",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "max-h-96 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
"className": "max-h-96 overflow-y-auto overflow-x-auto border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
@@ -207,13 +207,14 @@
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"comment": "min-w-[80px] 필수 — 표가 table-layout:auto 이고 td 의 min-width 가 0 이라, 좁은 뷰포트에서 은행명 열이 은행코드 열을 56px 로 압착하면 input 이 24px(= 좌우 padding 12+12)로 줄어 텍스트 영역이 0px 이 된다(입력한 값이 보이지 않음). width 지정은 auto 레이아웃이 무시하므로 하한(min-width)을 고정해야 한다. 숫자 스케일 min-w-16 등은 safelist 에 없어 CSS 가 생성되지 않으므로 쓰면 무효다.",
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "text",
|
||||
"value": "{{bank.code ?? ''}}",
|
||||
"placeholder": "$t:sirsoft-ecommerce.admin.settings.order_settings.bank_management.code_placeholder",
|
||||
"className": "{{$parent._local.bankModalErrors?.['banks.' + bank._idx + '.code'] ? 'input input-error' : 'input'}}",
|
||||
"className": "{{$parent._local.bankModalErrors?.['banks.' + bank._idx + '.code'] ? 'input input-error min-w-[80px]' : 'input min-w-[80px]'}}",
|
||||
"disabled": "{{$parent._computed.isReadOnly}}"
|
||||
},
|
||||
"actions": [
|
||||
|
||||
+24
@@ -74,6 +74,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "w-80(320px) 고정폭은 폰(<768px)의 콘텐츠 영역을 넘긴다(320px 뷰포트에서 32px 초과). 태블릿(768~1023px)은 넘치지 않으므로 portable 이 아니라 mobile(0~767px) 로 한정한다 — portable 로 잡으면 태블릿에서 입력칸이 불필요하게 늘어난다. className 오버라이드는 통째 교체라 에러 분기 표현식을 복제한다.",
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
@@ -82,6 +83,13 @@
|
||||
"placeholder": "$t:sirsoft-ecommerce.admin.settings.basic_info.shop_info.shop_name_placeholder",
|
||||
"className": "{{_local.errors?.['basic_info.shop_name'] ? 'input input-error w-80' : 'input w-80'}}",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
"responsive": {
|
||||
"mobile": {
|
||||
"props": {
|
||||
"className": "{{_local.errors?.['basic_info.shop_name'] ? 'input input-error w-full' : 'input w-full'}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -453,6 +461,7 @@
|
||||
"text": "$t:sirsoft-ecommerce.admin.settings.basic_info.business_info.business_category"
|
||||
},
|
||||
{
|
||||
"comment": "w-80(320px) 고정폭은 폰(<768px)의 콘텐츠 영역을 넘긴다(320px 뷰포트에서 32px 초과). 태블릿(768~1023px)은 넘치지 않으므로 portable 이 아니라 mobile(0~767px) 로 한정한다 — portable 로 잡으면 태블릿에서 입력칸이 불필요하게 늘어난다. className 오버라이드는 통째 교체라 에러 분기 표현식을 복제한다.",
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
@@ -461,6 +470,13 @@
|
||||
"placeholder": "$t:sirsoft-ecommerce.admin.settings.basic_info.business_info.business_category_placeholder",
|
||||
"className": "{{_local.errors?.['basic_info.business_category'] ? 'input input-error w-80' : 'input w-80'}}",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
"responsive": {
|
||||
"mobile": {
|
||||
"props": {
|
||||
"className": "{{_local.errors?.['basic_info.business_category'] ? 'input input-error w-full' : 'input w-full'}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -778,11 +794,19 @@
|
||||
"text": "$t:sirsoft-ecommerce.admin.settings.basic_info.business_info.email"
|
||||
},
|
||||
{
|
||||
"comment": "flex-center 는 nowrap 이고 자식이 고정폭(아이디 w-36 + @ + 도메인 입력 + '직접입력' 드롭다운 104px)이라 좁은 뷰포트에서 이 행이 콘텐츠 영역을 71px 넘긴다 → portable 에서 줄바꿈.",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-center gap-2"
|
||||
},
|
||||
"responsive": {
|
||||
"portable": {
|
||||
"props": {
|
||||
"className": "flex-center flex-wrap gap-2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
|
||||
+2
-1
@@ -394,11 +394,12 @@
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"comment": "overflow-x-auto 명시 — 6열 표(615px)가 좁은 뷰포트 컨테이너를 넘는다. overflow-y 만 선언해도 CSS Overflow 명세상 다른 축이 visible 로 남을 수 없어 computed 가 auto 로 계산되지만 그 의존은 암묵적이다. 누군가 overflow-y-visible 로 바꾸면 즉시 표가 잘리므로 의도를 코드에 고정한다. 표는 whitespace-nowrap 이라 절단 없이 가로 스크롤된다(세로 스택 전환은 과잉 대응).",
|
||||
"id": "template_list",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "max-h-96 overflow-y-auto"
|
||||
"className": "max-h-96 overflow-y-auto overflow-x-auto"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user