fix(core,ecommerce,board,basic,pay_kginicis,pay_nhnkcp,pay_nicepayments): 7.0.6 회귀 3건·잠복 결함 3건 해소 + 통화 하드코딩 전수 제거

7.0.6 사이클에서 유입된 회귀 3건을 출시 전에 닫는다. 추가옵션 저장소의
반환 타입 선언이 실제 반환(중첩 맵)과 어긋나 추가옵션 있는 상품의 담기·
바로구매가 죽었고, 검색 엔진이 결과 소비 시점에 SELECT 를 재작성해 정렬이
참조하던 스코어 별칭을 지웠으며, 미들웨어 targets 가 늘어난 라우트를
따라가지 못해 비회원 현금영수증이 전면 불능이었다. 셋 다 정적 검사의
사각지대였으므로 룰 2건과 계약 테스트를 함께 세워 재발을 막는다.

같은 유형의 잠복 결함 3건(빈 반환의 타입 불일치 2건, 없는 메서드 호출 1건)도
호출 경로가 생기는 순간 fatal 이므로 함께 닫았다.

브라우저 검수 중 드러난 선행 결함 5건을 이어서 수정한다. 엔진이 event 키로
선언한 액션을 React prop 으로 연결하지 않아 오류 없이 조용히 무반응이었고,
연결한 뒤에는 커스텀 컴포넌트의 이벤트 payload 가 유실됐다. 비회원 주문상세
응답에 현금영수증 키가 없어 발급 결과에 도달할 수 없었으며, 입금확인과
현금영수증 발급이 base 통화 금액을 써서 결제 통화가 다른 주문에서 각각
저장 실패와 잘못된 증빙을 냈다.

마지막으로 통화 단위를 코드·문구에서 걷어내 설정으로 이관한다. 다국어
문구의 원/円, 프론트의 KRW 고정표와 리터럴 폴백 173건, 서버 포맷의 통화
코드 리터럴이 대상이며, 기본 통화가 원이 아닌 상점에서 값은 맞고 단위만
틀린 금액이 예외 없이 나가던 문제였다. 통화 판정을 설정 단일 출처로 모으고
규칙 3건을 정적 검사로 고정했다.
This commit is contained in:
HeuJung
2026-08-10 09:07:46 +09:00
parent 7f4e665827
commit e21bff678f
167 changed files with 3980 additions and 696 deletions
@@ -22,6 +22,9 @@
### Fixed
- 관리자가 추가한 통화(예: 파운드)로 볼 때 금액이 원화 기호와 자릿수로 표시되던 문제를 수정했습니다. 이제 환경설정에 등록한 통화 기호와 소수 자릿수를 그대로 따릅니다.
- 표시 통화가 정해지지 않은 순간에 금액이 원화로 표시될 수 있던 문제를 수정했습니다. 이제 상점 기본 통화로 표시됩니다.
- 기본 통화가 원(KRW)이 아닌 쇼핑몰을 다른 통화로 볼 때, 추가옵션 금액만 기준 통화로 표시되고 그 값이 그대로 총 금액에 더해지던 문제를 수정했습니다. 상품 상세의 추가옵션 선택지·추가옵션 금액·총 금액, 장바구니와 주문서의 추가옵션 줄이 모두 보고 있는 통화로 표시됩니다. 기본 통화가 원인 상점은 이전과 동일합니다.
- 주문 상세의 결제 정보 영역에 결제 플러그인이 제공하는 공급가액·부가세·영수증 링크·에스크로 구매확정 버튼이 표시되지 않던 문제를 수정했습니다.
- 총 건수를 끝까지 세지 못한 목록에서 페이지 이동 막대가 사라지거나 "1페이지뿐" 으로 잘못 표시되던 문제를 수정했습니다. 마이페이지의 주문·마일리지·위시리스트·알림함, 게시판 목록, 상품 리뷰·문의, 내가 쓴 글 목록이 해당합니다.
- 검색 결과의 탭 배지가 끝까지 세지 못한 건수를 정확한 숫자처럼 보여주던 문제를 수정했습니다. 이제 그 경우 숫자 뒤에 "이상" 표시가 붙습니다.
@@ -30,7 +30,7 @@ describe('체크아웃 할인/쿠폰 선택 통화 환산 (_checkout_discount.js
it('쿠폰 옵션 텍스트는 multi_currency_benefit_formatted 선택통화 폴백을 쓴다 (옵션 텍스트 KRW 고정 차단)', () => {
// benefit_formatted 단독 사용 패턴이 없어야 한다(반드시 multi_currency 우선)
const benefitMatches = raw.match(/coupon\.benefit_formatted/g) ?? [];
const multiMatches = raw.match(/coupon\.multi_currency_benefit_formatted\?\.\[_global\.preferredCurrency \?\? 'KRW'\]/g) ?? [];
const multiMatches = raw.match(/coupon\.multi_currency_benefit_formatted\?\.\[_global\.preferredCurrency \?\? _global\.defaultCurrency\]/g) ?? [];
// 옵션 텍스트 2곳(주문쿠폰 + 배송비쿠폰) 모두 multi_currency 우선 폴백
expect(multiMatches.length).toBeGreaterThanOrEqual(2);
// benefit_formatted 가 등장하더라도 항상 multi_currency 폴백의 ?? 우변으로만
@@ -39,19 +39,19 @@ describe('체크아웃 할인/쿠폰 선택 통화 환산 (_checkout_discount.js
it('적용 주문쿠폰 할인액이 summary.multi_currency 선택통화 폴백을 쓴다', () => {
expect(raw).toContain(
"multi_currency?.[_global.preferredCurrency ?? 'KRW']?.order_coupon_discount_formatted"
"multi_currency?.[_global.preferredCurrency ?? _global.defaultCurrency]?.order_coupon_discount_formatted"
);
});
it('적용 배송비쿠폰 할인액이 summary.multi_currency 선택통화 폴백을 쓴다 (D5 핵심)', () => {
expect(raw).toContain(
"multi_currency?.[_global.preferredCurrency ?? 'KRW']?.shipping_discount_formatted"
"multi_currency?.[_global.preferredCurrency ?? _global.defaultCurrency]?.shipping_discount_formatted"
);
});
it('적용 할인코드 할인액이 summary.multi_currency 선택통화 폴백을 쓴다', () => {
expect(raw).toContain(
"multi_currency?.[_global.preferredCurrency ?? 'KRW']?.code_discount_formatted"
"multi_currency?.[_global.preferredCurrency ?? _global.defaultCurrency]?.code_discount_formatted"
);
});
});
@@ -77,15 +77,23 @@ describe('U10 — 다운로드 모달 무한스크롤 (버튼식 페이지네이
});
});
describe('U10 — 더보기 액션(증분 첫 페이지 로드)', () => {
describe('U10 — 더보기 액션(배지와 같은 목록을 연다)', () => {
const text = serialize(infoSummary);
it('더보기 apiCall 이 per_page=8 + page=1 로 첫 페이지를 로드한다', () => {
expect(text).toContain('per_page=8&page=1');
/*
* 「+N」 배지는 이 상품에 적용되는 쿠폰(productDownloadableCoupons)으로 계산된다.
* 더보기가 전체 다운로드 쿠폰을 새로 조회하면 이 상품에 적용되지 않는 쿠폰이 섞여
* 배지 수와 모달 건수가 어긋나므로, 더보기는 배지와 같은 데이터소스를 그대로 넘긴다.
* (종전 테스트는 별도 조회 설계를 고정하고 있었다 — 그 설계가 배지 불일치의 원인이었다.)
*/
it('더보기가 별도 조회 없이 배지와 같은 데이터소스를 모달에 넘긴다', () => {
expect(text).toContain('downloadableCoupons":"{{productDownloadableCoupons.data}}');
expect(text).not.toContain('per_page=8&page=1');
});
it('더보기 onSuccess 가 downloadingMore 를 초기화한다', () => {
expect(text).toContain('downloadingMore');
it('더보기가 모달 로딩 상태를 초기화한다', () => {
expect(text).toContain('downloadableCouponsLoading":false');
expect(text).toContain('downloadingMore":false');
});
it('downloadableCouponsPage setState 가 제거되었다', () => {
@@ -29,7 +29,7 @@ const modalText = fs.readFileSync(
describe('A1-④ — 쿠폰 모달 다통화 환산', () => {
it('정액 할인이 multi_currency_discount_value 선택통화 환산을 표시한다', () => {
expect(modalText).toContain('multi_currency_discount_value');
expect(modalText).toContain("_global.preferredCurrency ?? 'KRW'");
expect(modalText).toContain("_global.preferredCurrency ?? _global.defaultCurrency");
});
it('정액 할인 폴백은 기본통화 숫자 + currency_unit 을 유지한다', () => {
@@ -67,7 +67,7 @@ describe.each([
const strikethrough = nodes.find((n) =>
typeof n.text === 'string' && n.text.includes('multi_currency_list_price'));
expect(strikethrough).toBeDefined();
expect(strikethrough!.text).toContain("_global.preferredCurrency ?? 'KRW'");
expect(strikethrough!.text).toContain("_global.preferredCurrency ?? _global.defaultCurrency");
expect(strikethrough!.text).toContain('list_price_formatted');
expect(strikethrough!.props?.className).toContain('line-through');
});
@@ -83,7 +83,7 @@ describe.each([
expect(secondaryBlock).toBeDefined();
expect(secondaryBlock!.if).toContain('discount_rate');
expect(secondaryBlock!.iteration?.source).toContain('multi_currency_list_price');
expect(secondaryBlock!.iteration?.source).toContain("code !== (_global.preferredCurrency ?? 'KRW')");
expect(secondaryBlock!.iteration?.source).toContain("code !== (_global.preferredCurrency ?? _global.defaultCurrency)");
});
it('판매가 표시(multi_currency_selling_price)는 유지된다(비회귀)', () => {
@@ -30,12 +30,12 @@ describe('마이페이지 결제 정보 — base 통화 고정 + 결제통화
it('금액 primary 는 preferredCurrency 가 아니라 base(*_formatted)를 바인딩한다', () => {
// 회귀: mc_total_amount[preferredCurrency].formatted 를 primary 로 쓰던 패턴 제거
expect(s).not.toContain("mc_total_amount?.[_global.preferredCurrency ?? 'KRW']?.formatted");
expect(s).not.toContain("mc_total_amount?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted");
expect(s).toContain('order.data.total_amount_formatted');
});
it('보조 통화 iteration 은 주문 base_currency 를 제외한다', () => {
expect(s).toContain("order.data.base_currency ?? _global.preferredCurrency ?? 'KRW'");
expect(s).toContain("order.data.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency");
});
it('base≠결제 통화일 때 결제 통화 실청구액을 명시한다', () => {
@@ -69,7 +69,7 @@ describe('마이페이지 주문 목록 — base 통화 고정', () => {
it('합계 primary 는 base(*_formatted), 보조 통화는 base_currency 제외', () => {
expect(s).toContain('order.total_amount_formatted');
expect(s).toContain("order.base_currency ?? _global.preferredCurrency ?? 'KRW'");
expect(s).toContain("order.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency");
});
});
@@ -78,13 +78,13 @@ describe('마이페이지 주문 상세 상품 카드 — base 통화 고정', (
it('상품 단가/소계 primary 는 preferredCurrency 가 아니라 base(*_formatted)를 바인딩한다', () => {
// 회귀: mc_subtotal_price[preferredCurrency].formatted 를 primary 로 쓰던 패턴 제거
expect(s).not.toContain("mc_subtotal_price?.[_global.preferredCurrency ?? 'KRW']?.formatted");
expect(s).not.toContain("mc_unit_price?.[_global.preferredCurrency ?? 'KRW']?.formatted");
expect(s).not.toContain("mc_subtotal_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted");
expect(s).not.toContain("mc_unit_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted");
expect(s).toContain('item.unit_price_formatted');
expect(s).toContain('item.subtotal_price_formatted');
});
it('보조 통화 iteration 은 주문 base_currency 를 제외한다', () => {
expect(s).toContain("order.data.base_currency ?? _global.preferredCurrency ?? 'KRW'");
expect(s).toContain("order.data.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency");
});
});
@@ -60,7 +60,7 @@ describe('상품상세 총 금액 선택 통화 환산 (_purchase_card.json)', (
it('옵션 선택형 총 금액은 selectedTotalMultiCurrency 선택통화 포맷 맵을 조회한다 (D2)', () => {
expect(optionValueNode).toBeDefined();
expect(optionValueNode!.text).toContain("selectedTotalMultiCurrency?.[_global.preferredCurrency ?? 'KRW']?.formatted");
expect(optionValueNode!.text).toContain("selectedTotalMultiCurrency?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted");
});
it('옵션 선택형 총 금액은 handler() 표현식 호출과 원(KRW) 하드코딩을 쓰지 않는다 (금지패턴/D2 회귀 차단)', () => {
@@ -71,7 +71,7 @@ describe('상품상세 총 금액 선택 통화 환산 (_purchase_card.json)', (
it('옵션 없는 상품 총 금액은 noOptionTotalMultiCurrency 또는 단가 formatted 폴백을 쓴다 (D3)', () => {
expect(noOptionValueNode).toBeDefined();
expect(noOptionValueNode!.text).toContain("noOptionTotalMultiCurrency?.[_global.preferredCurrency ?? 'KRW']?.formatted");
expect(noOptionValueNode!.text).toContain("noOptionTotalMultiCurrency?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted");
// 단가 formatted 폴백(초기 수량 1)
expect(noOptionValueNode!.text).toContain('multi_currency_selling_price');
});
@@ -85,7 +85,7 @@ describe('상품상세 총 금액 선택 통화 환산 (_purchase_card.json)', (
it('개별 선택 항목 가격(multiCurrencyTotalPrice formatted)은 유지된다(비회귀)', () => {
const text = JSON.stringify(layout);
expect(text).toContain('multiCurrencyTotalPrice');
expect(text).toContain("?.[_global.preferredCurrency ?? 'KRW']?.formatted");
expect(text).toContain("?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted");
});
it('레이아웃 전체에 handler() 표현식 호출이 없다 (엔진 표현식 컨텍스트는 handler 미제공)', () => {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -58,5 +58,5 @@ export interface AvatarUploaderProps {
/** 레이아웃 편집기 주입 속성 (편집 모드 전용, 루트에 spread) */
editorAttrs?: EditorAttrs;
}
export declare function AvatarUploader({ src, fallbackText, size, uploadEndpoint, deleteEndpoint, onUploadSuccess, onUploadError, onDeleteSuccess, onDeleteError, showDeleteButton, accept, maxSize, className, uploadButtonText, deleteButtonText, readOnly, confirmDelete, deleteConfirmMessage, confirmUpload, uploadConfirmMessage, uploadSuccessActions, uploadErrorActions, deleteSuccessActions, deleteErrorActions, id, editorAttrs, }: AvatarUploaderProps): import("react").JSX.Element;
export declare function AvatarUploader({ src, fallbackText, size, uploadEndpoint, deleteEndpoint, onUploadSuccess, onUploadError, onDeleteSuccess, onDeleteError, showDeleteButton, accept, maxSize, className, uploadButtonText, deleteButtonText, readOnly, confirmDelete, deleteConfirmMessage, confirmUpload, uploadConfirmMessage, uploadSuccessActions, uploadErrorActions, deleteSuccessActions, deleteErrorActions, id, editorAttrs, }: AvatarUploaderProps): import("react/jsx-runtime").JSX.Element;
export default AvatarUploader;
@@ -7,23 +7,25 @@ interface FormatCurrencyParams {
/**
* 숫자 값을 통화 형식으로 포맷팅합니다.
*
* 원화 계열(₩/원)은 금액 뒤에 "원", 그 외는 기호를 앞에 붙입니다
* (백엔드 messages.currency.prefix/suffix 와 동일한 표기 규칙).
*
* @param params.value - 포맷팅할 숫자 값
* @param params.currencyCode - 통화 코드 (KRW, USD, JPY 등)
* @param params.locale - 로케일 (선택, 미지정 시 통화 기본 로케일 사용)
* @param params.currencyCode - 통화 코드 (미지정 시 표시 통화 → 기본 통화 순)
* @param params.locale - 로케일 (숫자 구분자용, 미지정 시 브라우저 로케일)
* @param context - 핸들러 컨텍스트
* @returns 포맷팅된 통화 문자열
*
* @example
* formatCurrencyHandler({ value: 10000, currencyCode: 'KRW' }, context)
* // => "₩10,000" 또는 "10,000원"
*
* formatCurrencyHandler({ value: 99.99, currencyCode: 'USD' }, context)
* // => "$99.99"
* formatCurrencyHandler({ value: 10000, currencyCode: 'KRW' }, context) // => "10,000원"
* formatCurrencyHandler({ value: 99.99, currencyCode: 'USD' }, context) // => "$99.99"
*/
export declare function formatCurrencyHandler(params: FormatCurrencyParams, context: HandlerContext): string;
/**
* 통화 심볼만 반환합니다.
*
* 설정의 symbol 을 우선하고, 없으면 폴백 표, 그것도 없으면 통화 코드를 그대로 돌려줍니다.
*
* @param currencyCode - 통화 코드
* @returns 통화 심볼
*/
@@ -21,7 +21,7 @@
"pattern": "/api/modules/sirsoft-ecommerce/*",
"headers": {
"X-Cart-Key": "{{_global.cartKey}}",
"X-Currency": "{{_global.preferredCurrency ?? _global.defaultCurrency ?? 'KRW'}}",
"X-Currency": "{{_global.preferredCurrency ?? _global.defaultCurrency}}",
"X-Shipping-Country": "{{_global.preferredShippingCountry ?? _global.modules?.['sirsoft-ecommerce']?.shipping?.default_country ?? 'KR'}}"
}
},
@@ -59,14 +59,14 @@
"handler": "setState",
"params": {
"target": "global",
"defaultCurrency": "{{_global.modules?.['sirsoft-ecommerce']?.language_currency?.default_currency ?? 'KRW'}}"
"defaultCurrency": "{{_global.modules?.['sirsoft-ecommerce']?.language_currency?.default_currency}}"
}
},
{
"_comment": "선호(표시) 통화를 _global 에 동기 주입. 우선순위 = 로그인 유저 계정 영속 통화(currentUser.ecommerce_preferred_currency, D-LOGIN-CUR) > localStorage 세션값(g7_preferred_currency, ISO형식) > 게스트 사이트 언어(g7_locale) 기반 통화 추정(currencies[].locales 매핑, 회원가입 통화 부여와 동일 입력·우선순위) > default_currency > KRW. 핸들러는 이커머스 모듈 소유(sirsoft-ecommerce.initPreferredCurrency) — 통화=커머스 책임. 모듈 미설치 시 미등록 핸들러로 무동작(통화 globals 자체가 빈 값).",
"handler": "sirsoft-ecommerce.initPreferredCurrency",
"params": {
"defaultCurrency": "{{_global.defaultCurrency ?? 'KRW'}}"
"defaultCurrency": "{{_global.defaultCurrency}}"
}
},
{
@@ -132,7 +132,7 @@
"handler": "sirsoft-ecommerce.initPreferredCurrency",
"params": {
"accountCurrency": "{{response.data?.data?.ecommerce_preferred_currency ?? response.data?.ecommerce_preferred_currency}}",
"defaultCurrency": "{{_global.defaultCurrency ?? 'KRW'}}"
"defaultCurrency": "{{_global.defaultCurrency}}"
}
},
{
@@ -1889,7 +1889,7 @@
"availableLocales": "{{$locales}}",
"currentLocale": "{{$locale}}",
"availableCurrencies": "{{_global.availableCurrencies ?? []}}",
"preferredCurrency": "{{_global.preferredCurrency ?? _global.defaultCurrency ?? 'KRW'}}",
"preferredCurrency": "{{_global.preferredCurrency ?? _global.defaultCurrency}}",
"className": "hidden",
"notifications": "{{(notifications?.data?.data ?? []).map(n => ({ id: n.id, title: n.subject ?? '', message: n.body ?? '', time: n.created_at ?? '', read: !!n.read_at, url: n.url ?? '', data: n.data ?? {} }))}}",
"notificationHasMore": "{{_global.notificationsHasMore ?? notifications?.data?.pagination?.has_more_pages ?? false}}",
@@ -5,22 +5,47 @@
},
"type": "basic",
"name": "Div",
"props": { "className": "relative" },
"props": {
"className": "relative"
},
"children": [
{
"type": "basic",
"name": "Button",
"props": { "className": "flex items-center gap-1.5 px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors" },
"props": {
"className": "flex items-center gap-1.5 px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
},
"children": [
{ "type": "basic", "name": "Icon", "props": { "name": "globe", "className": "w-4 h-4" } },
{ "type": "basic", "name": "Span", "text": "{{_global.preferredCurrency ?? 'KRW'}}" },
{ "type": "basic", "name": "Icon", "props": { "name": "chevron-down", "className": "w-3 h-3" } }
{
"type": "basic",
"name": "Icon",
"props": {
"name": "globe",
"className": "text-base"
}
},
{
"type": "basic",
"name": "Span",
"text": "{{_global.preferredCurrency ?? _global.defaultCurrency}}"
},
{
"type": "basic",
"name": "Icon",
"props": {
"name": "chevron-down",
"className": "text-xs"
}
}
],
"actions": [
{
"type": "click",
"handler": "setState",
"params": { "target": "local", "showCurrencyDropdown": "{{!_local.showCurrencyDropdown}}" }
"params": {
"target": "local",
"showCurrencyDropdown": "{{!_local.showCurrencyDropdown}}"
}
}
]
},
@@ -29,12 +54,17 @@
"type": "basic",
"name": "Div",
"if": "{{_local.showCurrencyDropdown}}",
"props": { "className": "absolute right-0 top-full mt-1 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" },
"props": {
"className": "absolute right-0 top-full mt-1 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"
},
"children": [
{
"type": "basic",
"name": "Div",
"iteration": { "source": "{{_global.availableCurrencies ?? []}}", "item_var": "currency" },
"iteration": {
"source": "{{_global.availableCurrencies ?? []}}",
"item_var": "currency"
},
"children": [
{
"type": "basic",
@@ -43,21 +73,47 @@
"className": "w-full px-4 py-2.5 text-left text-sm flex items-center gap-3 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors {{_global.preferredCurrency === currency.code ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300'}}"
},
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-base" }, "text": "{{currency.flag}}" },
{
"type": "basic",
"name": "Span",
"props": {
"className": "text-base"
},
"text": "{{currency.flag}}"
},
{
"type": "basic",
"name": "Div",
"props": { "className": "flex-1" },
"props": {
"className": "flex-1"
},
"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}})" }
{
"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}})"
}
]
},
{
"type": "basic",
"name": "Icon",
"if": "{{_global.preferredCurrency === currency.code}}",
"props": { "name": "check", "className": "w-4 h-4" }
"props": {
"name": "check",
"className": "text-base"
}
}
],
"actions": [
@@ -65,9 +121,27 @@
"type": "click",
"handler": "sequence",
"actions": [
{ "handler": "setState", "params": { "target": "global", "preferredCurrency": "{{currency.code}}" } },
{ "handler": "saveToLocalStorage", "params": { "key": "g7_preferred_currency", "value": "{{currency.code}}" } },
{ "handler": "setState", "params": { "target": "local", "showCurrencyDropdown": false } }
{
"handler": "setState",
"params": {
"target": "global",
"preferredCurrency": "{{currency.code}}"
}
},
{
"handler": "saveToLocalStorage",
"params": {
"key": "g7_preferred_currency",
"value": "{{currency.code}}"
}
},
{
"handler": "setState",
"params": {
"target": "local",
"showCurrencyDropdown": false
}
}
]
}
]
@@ -81,12 +155,17 @@
"type": "basic",
"name": "Div",
"if": "{{_local.showCurrencyDropdown}}",
"props": { "className": "fixed inset-0 z-40" },
"props": {
"className": "fixed inset-0 z-40"
},
"actions": [
{
"type": "click",
"handler": "setState",
"params": { "target": "local", "showCurrencyDropdown": false }
"params": {
"target": "local",
"showCurrencyDropdown": false
}
}
]
}
@@ -569,7 +569,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "text-base font-bold text-gray-900 dark:text-white ml-1" },
"text": "{{item.mc_final_amount?.[order.data.base_currency ?? _global.preferredCurrency ?? 'KRW']?.formatted ?? item.subtotal_price_formatted ?? ''}}",
"text": "{{item.mc_final_amount?.[order.data.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? item.subtotal_price_formatted ?? ''}}",
"responsive": {
"portable": {
"props": { "className": "text-sm font-bold text-gray-900 dark:text-white ml-1" }
@@ -597,7 +597,7 @@
"name": "Div",
"props": { "className": "flex flex-wrap gap-x-2 justify-end" },
"iteration": {
"source": "{{Object.entries(item.mc_final_amount ?? item.mc_subtotal_price ?? {}).filter(([code]) => code !== (order.data.base_currency ?? _global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(item.mc_final_amount ?? item.mc_subtotal_price ?? {}).filter(([code]) => code !== (order.data.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -824,7 +824,7 @@
"name": "Div",
"props": { "className": "flex flex-wrap gap-x-2 mt-0.5" },
"iteration": {
"source": "{{Object.entries(item.mc_subtotal_price ?? {}).filter(([code]) => code !== (order.base_currency ?? _global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(item.mc_subtotal_price ?? {}).filter(([code]) => code !== (order.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -908,7 +908,7 @@
"name": "Div",
"props": { "className": "flex flex-wrap gap-x-2" },
"iteration": {
"source": "{{Object.entries(order.mc_total_amount ?? {}).filter(([code]) => code !== (order.base_currency ?? _global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(order.mc_total_amount ?? {}).filter(([code]) => code !== (order.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -399,7 +399,7 @@
"name": "Span",
"props": { "className": "text-xs text-gray-400 dark:text-gray-500 block" },
"iteration": {
"source": "{{Object.entries(_local.refundEstimate?.mc_original_snapshot?.mc_total_list_price_amount_formatted ?? {}).filter(([code]) => code !== (_local.refundEstimate?.original_snapshot?.base_currency ?? _global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(_local.refundEstimate?.mc_original_snapshot?.mc_total_list_price_amount_formatted ?? {}).filter(([code]) => code !== (_local.refundEstimate?.original_snapshot?.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "mc"
},
"text": "{{mc[1]}}"
@@ -423,7 +423,7 @@
"name": "Span",
"props": { "className": "text-xs text-gray-400 dark:text-gray-500 block" },
"iteration": {
"source": "{{Object.entries(_local.refundEstimate?.mc_recalculated_snapshot?.mc_total_list_price_amount_formatted ?? {}).filter(([code]) => code !== (_local.refundEstimate?.original_snapshot?.base_currency ?? _global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(_local.refundEstimate?.mc_recalculated_snapshot?.mc_total_list_price_amount_formatted ?? {}).filter(([code]) => code !== (_local.refundEstimate?.original_snapshot?.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "mc"
},
"text": "{{mc[1]}}"
@@ -456,7 +456,7 @@
"name": "Span",
"props": { "className": "text-xs text-gray-400 dark:text-gray-500 block" },
"iteration": {
"source": "{{Object.entries(_local.refundEstimate?.mc_original_snapshot?.mc_subtotal_amount_formatted ?? {}).filter(([code]) => code !== (_local.refundEstimate?.original_snapshot?.base_currency ?? _global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(_local.refundEstimate?.mc_original_snapshot?.mc_subtotal_amount_formatted ?? {}).filter(([code]) => code !== (_local.refundEstimate?.original_snapshot?.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "mc"
},
"text": "{{mc[1]}}"
@@ -480,7 +480,7 @@
"name": "Span",
"props": { "className": "text-xs text-gray-400 dark:text-gray-500 block" },
"iteration": {
"source": "{{Object.entries(_local.refundEstimate?.mc_recalculated_snapshot?.mc_subtotal_amount_formatted ?? {}).filter(([code]) => code !== (_local.refundEstimate?.original_snapshot?.base_currency ?? _global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(_local.refundEstimate?.mc_recalculated_snapshot?.mc_subtotal_amount_formatted ?? {}).filter(([code]) => code !== (_local.refundEstimate?.original_snapshot?.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "mc"
},
"text": "{{mc[1]}}"
@@ -771,7 +771,7 @@
"name": "Span",
"props": { "className": "text-xs text-gray-400 dark:text-gray-500 block font-normal" },
"iteration": {
"source": "{{Object.entries(_local.refundEstimate?.mc_original_snapshot?.mc_total_paid_amount_formatted ?? {}).filter(([code]) => code !== (_local.refundEstimate?.original_snapshot?.base_currency ?? _global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(_local.refundEstimate?.mc_original_snapshot?.mc_total_paid_amount_formatted ?? {}).filter(([code]) => code !== (_local.refundEstimate?.original_snapshot?.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "mc"
},
"text": "{{mc[1]}}"
@@ -795,7 +795,7 @@
"name": "Span",
"props": { "className": "text-xs text-gray-400 dark:text-gray-500 block font-normal" },
"iteration": {
"source": "{{Object.entries(_local.refundEstimate?.mc_recalculated_snapshot?.mc_total_paid_amount_formatted ?? {}).filter(([code]) => code !== (_local.refundEstimate?.original_snapshot?.base_currency ?? _global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(_local.refundEstimate?.mc_recalculated_snapshot?.mc_total_paid_amount_formatted ?? {}).filter(([code]) => code !== (_local.refundEstimate?.original_snapshot?.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "mc"
},
"text": "{{mc[1]}}"
@@ -541,7 +541,7 @@
"name": "Div",
"props": { "className": "flex flex-wrap gap-x-2 justify-end mt-0.5" },
"iteration": {
"source": "{{Object.entries(order.data.mc_total_amount ?? {}).filter(([code]) => code !== (order.data.base_currency ?? _global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(order.data.mc_total_amount ?? {}).filter(([code]) => code !== (order.data.base_currency ?? _global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -320,7 +320,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "text-sm font-medium {{Number(shipping.total_shipping_amount ?? 0) === 0 ? 'text-green-600 dark:text-green-400' : 'text-gray-900 dark:text-white'}}" },
"text": "{{Number(shipping.total_shipping_amount ?? 0) === 0 ? '$t:mypage.order_detail.free_shipping' : (shipping.mc_total_shipping_amount?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? shipping.total_shipping_amount_formatted ?? '')}}"
"text": "{{Number(shipping.total_shipping_amount ?? 0) === 0 ? '$t:mypage.order_detail.free_shipping' : (shipping.mc_total_shipping_amount?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? shipping.total_shipping_amount_formatted ?? '')}}"
}
]
},
@@ -1379,7 +1379,7 @@
"name": "Select",
"props": {
"name": "ecommerce_preferred_currency",
"value": "{{user.data?.ecommerce_preferred_currency ?? _global.modules?.['sirsoft-ecommerce']?.language_currency?.default_currency ?? 'KRW'}}",
"value": "{{user.data?.ecommerce_preferred_currency ?? _global.modules?.['sirsoft-ecommerce']?.language_currency?.default_currency}}",
"options": "{{(_global.modules?.['sirsoft-ecommerce']?.language_currency?.currencies ?? []).filter(c => c.is_default || (c.exchange_rate ?? 0) > 0).map(c => ({ value: c.code, label: c.code + ' (' + (c.symbol ?? c.code) + ')' }))}}",
"className": "w-full sm:w-64"
}
@@ -779,7 +779,7 @@
"props": {
"className": "text-base text-gray-900 dark:text-white font-medium"
},
"text": "{{user.data?.ecommerce_preferred_currency ?? _global.modules?.['sirsoft-ecommerce']?.language_currency?.default_currency ?? 'KRW'}}"
"text": "{{user.data?.ecommerce_preferred_currency ?? _global.modules?.['sirsoft-ecommerce']?.language_currency?.default_currency}}"
},
{
"type": "basic",
@@ -54,14 +54,14 @@
"type": "basic",
"name": "Span",
"props": { "className": "font-bold text-gray-900 dark:text-white" },
"text": "{{product.multi_currency_selling_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? product.selling_price_formatted}}"
"text": "{{product.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? product.selling_price_formatted}}"
},
{
"type": "basic",
"name": "Span",
"if": "{{product.discount_rate > 0}}",
"props": { "className": "text-sm text-gray-500 dark:text-gray-400 line-through" },
"text": "{{product.multi_currency_list_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? product.list_price_formatted}}"
"text": "{{product.multi_currency_list_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? product.list_price_formatted}}"
}
]
}
@@ -82,7 +82,7 @@
"type": "basic",
"name": "P",
"props": { "className": "text-xs text-gray-500 dark:text-gray-400" },
"text": "{{addOpt.group_name}}: {{addOpt.name}}{{(addOpt.custom_text ?? '') !== '' ? ' (' + addOpt.custom_text + ')' : ''}}{{(addOpt.price_adjustment ?? 0) > 0 ? ' (' + (addOpt.price_adjustment_formatted ?? ('+' + addOpt.price_adjustment.toLocaleString())) + ')' : ''}}"
"text": "{{addOpt.group_name}}: {{addOpt.name}}{{(addOpt.custom_text ?? '') !== '' ? ' (' + addOpt.custom_text + ')' : ''}}{{(addOpt.price_adjustment ?? 0) > 0 ? ' (' + (addOpt.multi_currency_price_adjustment?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? addOpt.price_adjustment_formatted ?? ('+' + addOpt.price_adjustment.toLocaleString())) + ')' : ''}}"
}
]
},
@@ -145,7 +145,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "text-xs text-gray-400 dark:text-gray-500 line-through" },
"text": "{{item.product_option?.multi_currency_list_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? item.product_option?.list_price_formatted ?? ''}}"
"text": "{{item.product_option?.multi_currency_list_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? item.product_option?.list_price_formatted ?? ''}}"
},
{
"comment": "할인율 칩",
@@ -163,7 +163,7 @@
"if": "{{(item.product_option?.discount_rate ?? 0) > 0}}",
"props": { "className": "flex flex-wrap gap-x-2" },
"iteration": {
"source": "{{Object.entries(item.product_option?.multi_currency_list_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(item.product_option?.multi_currency_list_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -180,7 +180,7 @@
"type": "basic",
"name": "Div",
"props": { "className": "text-sm font-medium text-gray-900 dark:text-white" },
"text": "{{item.product_option?.multi_currency_selling_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? item.product_option?.selling_price_formatted ?? ''}}"
"text": "{{item.product_option?.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? item.product_option?.selling_price_formatted ?? ''}}"
},
{
"comment": "보조 통화들 (선택된 통화 제외)",
@@ -188,7 +188,7 @@
"name": "Div",
"props": { "className": "flex flex-wrap gap-x-2" },
"iteration": {
"source": "{{Object.entries(item.product_option?.multi_currency_selling_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(item.product_option?.multi_currency_selling_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -400,7 +400,7 @@
"type": "basic",
"name": "Div",
"props": { "className": "font-bold text-gray-900 dark:text-white" },
"text": "{{item.multi_currency_subtotal?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? item.subtotal_formatted}}"
"text": "{{item.multi_currency_subtotal?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? item.subtotal_formatted}}"
},
{
"comment": "보조 통화들",
@@ -408,7 +408,7 @@
"name": "Div",
"props": { "className": "flex flex-wrap gap-x-2 justify-end" },
"iteration": {
"source": "{{Object.entries(item.multi_currency_subtotal ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(item.multi_currency_subtotal ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -118,7 +118,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "font-medium text-gray-900 dark:text-white" },
"text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.subtotal_formatted ?? cartItems.data.calculation?.summary?.subtotal_formatted ?? '-'}}"
"text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.subtotal_formatted ?? cartItems.data.calculation?.summary?.subtotal_formatted ?? '-'}}"
}
]
},
@@ -136,7 +136,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.taxable_amount" },
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.taxable_amount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.taxable_amount_formatted ?? '-'}}" }
]
},
{
@@ -146,7 +146,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.tax_free_amount" },
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.tax_free_amount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.tax_free_amount_formatted ?? '-'}}" }
]
}
]
@@ -165,7 +165,7 @@
"props": { "className": "flex justify-between text-sm" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "$t:shop.cart.discount_total" },
{ "type": "basic", "name": "Span", "props": { "className": "font-medium text-red-600 dark:text-red-400" }, "text": "-{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.total_discount_formatted ?? cartItems.data.calculation?.summary?.discount_formatted}}" }
{ "type": "basic", "name": "Span", "props": { "className": "font-medium text-red-600 dark:text-red-400" }, "text": "-{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.total_discount_formatted ?? cartItems.data.calculation?.summary?.discount_formatted}}" }
]
},
{
@@ -181,7 +181,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.coupon_discount" },
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.product_coupon_discount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.product_coupon_discount_formatted ?? '-'}}" }
]
},
{
@@ -191,7 +191,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.code_discount" },
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.code_discount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.code_discount_formatted ?? '-'}}" }
]
},
{
@@ -201,7 +201,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.order_discount" },
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.order_coupon_discount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.order_coupon_discount_formatted ?? '-'}}" }
]
}
]
@@ -223,7 +223,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "font-medium {{(cartItems.data.calculation?.summary?.total_shipping ?? 0) === 0 ? 'text-green-600 dark:text-green-400' : 'text-gray-900 dark:text-white'}}" },
"text": "{{(cartItems.data.calculation?.summary?.total_shipping ?? 0) === 0 ? '$t:shop.free_shipping' : (cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.total_shipping_formatted ?? cartItems.data.calculation?.summary?.shipping_fee_formatted)}}"
"text": "{{(cartItems.data.calculation?.summary?.total_shipping ?? 0) === 0 ? '$t:shop.free_shipping' : (cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.total_shipping_formatted ?? cartItems.data.calculation?.summary?.shipping_fee_formatted)}}"
}
]
},
@@ -241,7 +241,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.base_shipping" },
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.base_shipping_total_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.base_shipping_total_formatted ?? '-'}}" }
]
},
{
@@ -251,7 +251,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.extra_shipping" },
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.extra_shipping_total_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.extra_shipping_total_formatted ?? '-'}}" }
]
},
{
@@ -261,7 +261,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.shipping_discount" },
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.shipping_discount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.shipping_discount_formatted ?? '-'}}" }
]
}
]
@@ -275,7 +275,7 @@
"props": { "className": "flex justify-between text-sm pt-2" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "$t:shop.cart.payment_amount" },
{ "type": "basic", "name": "Span", "props": { "className": "font-medium text-gray-900 dark:text-white" }, "text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.payment_amount_formatted ?? cartItems.data.calculation?.summary?.payment_amount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "font-medium text-gray-900 dark:text-white" }, "text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.payment_amount_formatted ?? cartItems.data.calculation?.summary?.payment_amount_formatted ?? '-'}}" }
]
},
{
@@ -303,7 +303,7 @@
"props": { "className": "flex justify-between items-center" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-lg font-semibold text-gray-900 dark:text-white" }, "text": "$t:shop.cart.total" },
{ "type": "basic", "name": "Span", "props": { "className": "text-xl font-bold text-gray-900 dark:text-white" }, "text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.final_amount_formatted ?? cartItems.data.calculation?.summary?.final_amount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-xl font-bold text-gray-900 dark:text-white" }, "text": "{{cartItems.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.final_amount_formatted ?? cartItems.data.calculation?.summary?.final_amount_formatted ?? '-'}}" }
]
},
{
@@ -312,7 +312,7 @@
"name": "Div",
"props": { "className": "flex flex-wrap gap-x-2 justify-end mt-1" },
"iteration": {
"source": "{{Object.entries(cartItems.data.calculation?.summary?.multi_currency ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(cartItems.data.calculation?.summary?.multi_currency ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -167,7 +167,7 @@
"props": {
"value": "{{coupon.id}}"
},
"text": "{{'[' + coupon.target_type_short_label + '] ' + coupon.localized_name + ' (' + (coupon.multi_currency_benefit_formatted?.[_global.preferredCurrency ?? 'KRW'] ?? coupon.benefit_formatted) + ')'}}"
"text": "{{'[' + coupon.target_type_short_label + '] ' + coupon.localized_name + ' (' + (coupon.multi_currency_benefit_formatted?.[_global.preferredCurrency ?? _global.defaultCurrency] ?? coupon.benefit_formatted) + ')'}}"
}
],
"actions": [
@@ -225,7 +225,7 @@
"props": { "className": "mt-2 text-sm text-green-600 dark:text-green-400 flex items-center gap-1" },
"children": [
{ "type": "basic", "name": "Icon", "props": { "name": "check-circle", "size": "sm" } },
{ "type": "basic", "name": "Span", "text": "-{{checkoutData.data.calculation?.summary?.multi_currency?.[_global.preferredCurrency ?? 'KRW']?.order_coupon_discount_formatted ?? checkoutData.data.calculation?.summary?.order_coupon_discount_formatted}}" }
{ "type": "basic", "name": "Span", "text": "-{{checkoutData.data.calculation?.summary?.multi_currency?.[_global.preferredCurrency ?? _global.defaultCurrency]?.order_coupon_discount_formatted ?? checkoutData.data.calculation?.summary?.order_coupon_discount_formatted}}" }
]
}
]
@@ -291,7 +291,7 @@
"props": {
"value": "{{coupon.id}}"
},
"text": "{{'[' + coupon.target_type_short_label + '] ' + coupon.localized_name + ' (' + (coupon.multi_currency_benefit_formatted?.[_global.preferredCurrency ?? 'KRW'] ?? coupon.benefit_formatted) + ')'}}"
"text": "{{'[' + coupon.target_type_short_label + '] ' + coupon.localized_name + ' (' + (coupon.multi_currency_benefit_formatted?.[_global.preferredCurrency ?? _global.defaultCurrency] ?? coupon.benefit_formatted) + ')'}}"
}
],
"actions": [
@@ -349,7 +349,7 @@
"props": { "className": "mt-2 text-sm text-green-600 dark:text-green-400 flex items-center gap-1" },
"children": [
{ "type": "basic", "name": "Icon", "props": { "name": "check-circle", "size": "sm" } },
{ "type": "basic", "name": "Span", "text": "-{{checkoutData.data.calculation?.summary?.multi_currency?.[_global.preferredCurrency ?? 'KRW']?.shipping_discount_formatted ?? checkoutData.data.calculation?.summary?.shipping_discount_formatted ?? (checkoutData.data.calculation?.summary?.shipping_discount + '원')}}" }
{ "type": "basic", "name": "Span", "text": "-{{checkoutData.data.calculation?.summary?.multi_currency?.[_global.preferredCurrency ?? _global.defaultCurrency]?.shipping_discount_formatted ?? checkoutData.data.calculation?.summary?.shipping_discount_formatted ?? (checkoutData.data.calculation?.summary?.shipping_discount + '원')}}" }
]
}
]
@@ -459,7 +459,7 @@
"props": { "className": "mt-2 text-sm text-green-600 dark:text-green-400 flex items-center gap-1" },
"children": [
{ "type": "basic", "name": "Icon", "props": { "name": "check-circle", "size": "sm" } },
{ "type": "basic", "name": "Span", "text": "-{{checkoutData.data.calculation?.summary?.multi_currency?.[_global.preferredCurrency ?? 'KRW']?.code_discount_formatted ?? checkoutData.data.calculation?.summary?.code_discount_formatted}}" }
{ "type": "basic", "name": "Span", "text": "-{{checkoutData.data.calculation?.summary?.multi_currency?.[_global.preferredCurrency ?? _global.defaultCurrency]?.code_discount_formatted ?? checkoutData.data.calculation?.summary?.code_discount_formatted}}" }
]
}
]
@@ -140,7 +140,7 @@
"type": "basic",
"name": "P",
"props": { "className": "text-xs text-gray-500 dark:text-gray-400" },
"text": "{{addOpt.group_name ? addOpt.group_name + ': ' : ''}}{{addOpt.name}}{{(addOpt.custom_text ?? '') !== '' ? ' (' + addOpt.custom_text + ')' : ''}}{{(addOpt.price_adjustment ?? 0) > 0 ? ' (' + (addOpt.price_adjustment_formatted ?? ('+' + addOpt.price_adjustment.toLocaleString())) + ')' : ''}}"
"text": "{{addOpt.group_name ? addOpt.group_name + ': ' : ''}}{{addOpt.name}}{{(addOpt.custom_text ?? '') !== '' ? ' (' + addOpt.custom_text + ')' : ''}}{{(addOpt.price_adjustment ?? 0) > 0 ? ' (' + (addOpt.multi_currency_price_adjustment?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? addOpt.price_adjustment_formatted ?? ('+' + addOpt.price_adjustment.toLocaleString())) + ')' : ''}}"
}
]
},
@@ -203,7 +203,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "text-xs text-gray-400 dark:text-gray-500 line-through" },
"text": "{{item.product_option?.multi_currency_list_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? item.product_option?.list_price_formatted ?? ''}}"
"text": "{{item.product_option?.multi_currency_list_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? item.product_option?.list_price_formatted ?? ''}}"
},
{
"comment": "할인율 칩",
@@ -221,7 +221,7 @@
"if": "{{(item.product_option?.discount_rate ?? 0) > 0}}",
"props": { "className": "flex flex-wrap gap-x-2" },
"iteration": {
"source": "{{Object.entries(item.product_option?.multi_currency_list_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(item.product_option?.multi_currency_list_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -247,7 +247,7 @@
{
"type": "basic",
"name": "Span",
"text": "{{item.product_option?.multi_currency_selling_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? item.product_option?.selling_price_formatted ?? ''}} × {{item.quantity}}"
"text": "{{item.product_option?.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? item.product_option?.selling_price_formatted ?? ''}} × {{item.quantity}}"
},
{
"type": "basic",
@@ -262,7 +262,7 @@
"name": "Div",
"props": { "className": "flex flex-wrap gap-x-2 mt-0.5" },
"iteration": {
"source": "{{Object.entries(item.product_option?.multi_currency_selling_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(item.product_option?.multi_currency_selling_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -555,7 +555,7 @@
"name": "Div",
"if": "{{(item.product_coupon_discount_amount ?? 0) === 0}}",
"props": { "className": "font-bold text-gray-900 dark:text-white" },
"text": "{{item.multi_currency_subtotal?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? item.subtotal_formatted}}"
"text": "{{item.multi_currency_subtotal?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? item.subtotal_formatted}}"
},
{
"comment": "원 판매금액 (할인 있을 때 취소선) - 주 통화",
@@ -563,7 +563,7 @@
"name": "Div",
"if": "{{(item.product_coupon_discount_amount ?? 0) > 0}}",
"props": { "className": "text-sm text-gray-400 dark:text-gray-500 line-through" },
"text": "{{item.multi_currency_subtotal?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? item.subtotal_formatted}}"
"text": "{{item.multi_currency_subtotal?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? item.subtotal_formatted}}"
},
{
"comment": "할인 후 금액 - 주 통화",
@@ -571,7 +571,7 @@
"name": "Div",
"if": "{{(item.product_coupon_discount_amount ?? 0) > 0}}",
"props": { "className": "font-bold text-red-600 dark:text-red-400" },
"text": "{{item.multi_currency_final_amount?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? item.final_amount_formatted}}"
"text": "{{item.multi_currency_final_amount?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? item.final_amount_formatted}}"
},
{
"comment": "원 판매금액 - 보조 통화들 (할인 없을 때)",
@@ -580,7 +580,7 @@
"if": "{{(item.product_coupon_discount_amount ?? 0) === 0}}",
"props": { "className": "flex flex-wrap gap-x-2 justify-end" },
"iteration": {
"source": "{{Object.entries(item.multi_currency_subtotal ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(item.multi_currency_subtotal ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "subtotalCurrency"
},
"children": [
@@ -599,7 +599,7 @@
"if": "{{(item.product_coupon_discount_amount ?? 0) > 0}}",
"props": { "className": "flex flex-wrap gap-x-2 justify-end" },
"iteration": {
"source": "{{Object.entries(item.multi_currency_final_amount ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(item.multi_currency_final_amount ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "finalCurrency"
},
"children": [
@@ -82,7 +82,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "font-medium text-gray-900 dark:text-white" },
"text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.subtotal_formatted ?? checkoutData.data.calculation?.summary?.subtotal_formatted ?? '-'}}"
"text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.subtotal_formatted ?? checkoutData.data.calculation?.summary?.subtotal_formatted ?? '-'}}"
}
]
},
@@ -100,7 +100,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.taxable_amount" },
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.taxable_amount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.taxable_amount_formatted ?? '-'}}" }
]
},
{
@@ -110,7 +110,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.tax_free_amount" },
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.tax_free_amount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.tax_free_amount_formatted ?? '-'}}" }
]
}
]
@@ -151,7 +151,7 @@
}
]
},
{ "type": "basic", "name": "Span", "props": { "className": "font-medium text-red-600 dark:text-red-400" }, "text": "-{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.total_discount_formatted ?? checkoutData.data.calculation?.summary?.total_discount_formatted}}" }
{ "type": "basic", "name": "Span", "props": { "className": "font-medium text-red-600 dark:text-red-400" }, "text": "-{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.total_discount_formatted ?? checkoutData.data.calculation?.summary?.total_discount_formatted}}" }
]
},
{
@@ -168,7 +168,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.coupon_discount" },
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.product_coupon_discount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.product_coupon_discount_formatted ?? '-'}}" }
]
},
{
@@ -200,7 +200,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.code_discount" },
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.code_discount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.code_discount_formatted ?? '-'}}" }
]
},
{
@@ -210,7 +210,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.order_discount" },
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.order_coupon_discount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.order_coupon_discount_formatted ?? '-'}}" }
]
},
{
@@ -254,7 +254,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "font-medium {{(checkoutData.data.calculation?.summary?.total_shipping ?? 0) === 0 ? 'text-green-600 dark:text-green-400' : 'text-gray-900 dark:text-white'}}" },
"text": "{{(checkoutData.data.calculation?.summary?.total_shipping ?? 0) === 0 ? '$t:shop.checkout.free' : (checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.total_shipping_formatted ?? checkoutData.data.calculation?.summary?.total_shipping_formatted)}}"
"text": "{{(checkoutData.data.calculation?.summary?.total_shipping ?? 0) === 0 ? '$t:shop.checkout.free' : (checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.total_shipping_formatted ?? checkoutData.data.calculation?.summary?.total_shipping_formatted)}}"
}
]
},
@@ -272,7 +272,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.base_shipping" },
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.base_shipping_total_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.base_shipping_total_formatted ?? '-'}}" }
]
},
{
@@ -282,7 +282,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.extra_shipping" },
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.extra_shipping_total_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.extra_shipping_total_formatted ?? '-'}}" }
]
},
{
@@ -292,7 +292,7 @@
"props": { "className": "flex justify-between text-xs" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-500 dark:text-gray-500" }, "text": "$t:shop.cart.shipping_discount" },
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.shipping_discount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-red-500 dark:text-red-400" }, "text": "-{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.shipping_discount_formatted ?? '-'}}" }
]
},
{
@@ -339,7 +339,7 @@
"props": { "className": "flex justify-between text-sm pt-2" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "$t:shop.cart.payment_amount" },
{ "type": "basic", "name": "Span", "props": { "className": "font-medium text-gray-900 dark:text-white" }, "text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.payment_amount_formatted ?? checkoutData.data.calculation?.summary?.payment_amount_formatted ?? '-'}}" }
{ "type": "basic", "name": "Span", "props": { "className": "font-medium text-gray-900 dark:text-white" }, "text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.payment_amount_formatted ?? checkoutData.data.calculation?.summary?.payment_amount_formatted ?? '-'}}" }
]
},
{
@@ -391,7 +391,7 @@
"props": { "className": "text-lg font-bold text-gray-900 dark:text-white" }
}
},
"text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.final_amount_formatted ?? checkoutData.data.calculation?.summary?.final_amount_formatted ?? '-'}}"
"text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.final_amount_formatted ?? checkoutData.data.calculation?.summary?.final_amount_formatted ?? '-'}}"
}
]
},
@@ -401,7 +401,7 @@
"name": "Div",
"props": { "className": "flex flex-wrap gap-x-2 justify-end mt-1" },
"iteration": {
"source": "{{Object.entries(checkoutData.data.calculation?.summary?.multi_currency ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(checkoutData.data.calculation?.summary?.multi_currency ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -439,7 +439,7 @@
}
}
},
"text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? 'KRW']?.final_amount_formatted ?? checkoutData.data.calculation?.summary?.final_amount_formatted ?? '-'}} $t:shop.checkout.pay_button",
"text": "{{checkoutData.data.calculation?.summary?.multi_currency[_global.preferredCurrency ?? _global.defaultCurrency]?.final_amount_formatted ?? checkoutData.data.calculation?.summary?.final_amount_formatted ?? '-'}} $t:shop.checkout.pay_button",
"actions": [
{
"type": "click",
@@ -105,7 +105,7 @@
"type": "basic",
"name": "P",
"props": { "className": "text-sm font-medium text-gray-900 dark:text-white" },
"text": "{{$parent._local.optionModal.targetItem?.multi_currency_selling_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? $parent._local.optionModal.targetItem?.unit_price_formatted}}"
"text": "{{$parent._local.optionModal.targetItem?.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? $parent._local.optionModal.targetItem?.unit_price_formatted}}"
},
{
"comment": "보조 통화들",
@@ -113,7 +113,7 @@
"name": "Div",
"props": { "className": "flex flex-wrap gap-x-2" },
"iteration": {
"source": "{{Object.entries($parent._local.optionModal.targetItem?.multi_currency_selling_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries($parent._local.optionModal.targetItem?.multi_currency_selling_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -277,7 +277,7 @@
"className": "w-full",
"value": "{{$parent._local.additionalSelections?.[addGroup.id] ?? ''}}",
"placeholder": "$t:shop.additional_option_placeholder",
"options": "{{[{ value: '', label: $t('shop.additional_option_placeholder') }, ...(addGroup.values ?? []).map(v => ({ value: v.id, label: v.price_adjustment > 0 ? v.name + ' (' + (v.price_adjustment_formatted ?? ('+' + v.price_adjustment.toLocaleString())) + ')' : v.name }))]}}"
"options": "{{[{ value: '', label: $t('shop.additional_option_placeholder') }, ...(addGroup.values ?? []).map(v => ({ value: v.id, label: v.price_adjustment > 0 ? v.name + ' (' + (v.multi_currency_price_adjustment?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? v.price_adjustment_formatted ?? ('+' + v.price_adjustment.toLocaleString())) + ')' : v.name }))]}}"
},
"actions": [
{
@@ -355,7 +355,7 @@
"type": "basic",
"name": "Div",
"props": { "className": "text-sm font-bold text-gray-900 dark:text-white" },
"text": "{{$parent._local.matchedOption.multi_currency_selling_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? $parent._local.matchedOption.selling_price_formatted}}"
"text": "{{$parent._local.matchedOption.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? $parent._local.matchedOption.selling_price_formatted}}"
},
{
"comment": "보조 통화들",
@@ -363,7 +363,7 @@
"name": "Div",
"props": { "className": "flex flex-wrap gap-x-2 justify-end" },
"iteration": {
"source": "{{Object.entries($parent._local.matchedOption.multi_currency_selling_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries($parent._local.matchedOption.multi_currency_selling_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -387,7 +387,7 @@
"props": { "className": "flex justify-between items-center mt-2 pt-2 border-t border-blue-100 dark:border-blue-800 text-xs text-gray-600 dark:text-gray-300" },
"children": [
{ "type": "basic", "name": "Span", "text": "$t:shop.additional_options_amount" },
{ "type": "basic", "name": "Span", "text": "{{(() => { const total = ($parent._local.optionProduct?.additional_options ?? []).reduce((sum, g) => sum + (((g.values ?? []).find(v => v.id === Number($parent._local.additionalSelections?.[g.id]))?.price_adjustment) ?? 0), 0); const c = (_global.availableCurrencies ?? []).find(x => x.is_default); const dp = c?.decimal_places ?? 0; const n = total.toLocaleString(undefined, { minimumFractionDigits: dp, maximumFractionDigits: dp }); const s = c?.symbol ?? '₩'; return '+' + ((c?.code === 'KRW' || s === '₩' || s === '원') ? (n + '원') : (s + n)); })()}}" }
{ "comment": "추가금은 기본통화 기준 저장값이므로 서버가 준 표시통화 환산값을 우선 합산한다 (없으면 기본통화 값으로 폴백)", "type": "basic", "name": "Span", "text": "{{(() => { const code = _global.preferredCurrency ?? _global.defaultCurrency; const total = ($parent._local.optionProduct?.additional_options ?? []).reduce((sum, g) => { const v = (g.values ?? []).find(v => v.id === Number($parent._local.additionalSelections?.[g.id])); if (!v) return sum; return sum + (v.multi_currency_price_adjustment?.[code]?.price ?? v.price_adjustment ?? 0); }, 0); const cur = (_global.availableCurrencies ?? []).find(x => x.code === code) ?? (_global.availableCurrencies ?? []).find(x => x.is_default); const dp = cur?.decimal_places ?? 0; const n = total.toLocaleString(undefined, { minimumFractionDigits: dp, maximumFractionDigits: dp }); const s = cur?.symbol ?? '₩'; return '+' + ((cur?.code === 'KRW' || s === '₩' || s === '원') ? (n + '원') : (s + n)); })()}}" }
]
},
{
@@ -154,15 +154,15 @@
"comment": "정액 할인 — 선택 통화 환산 (A1-④, formatted 가 기호 포함)",
"type": "basic",
"name": "Span",
"if": "{{coupon.discount_type === 'fixed' && coupon.multi_currency_discount_value?.[_global.preferredCurrency ?? 'KRW']?.formatted}}",
"if": "{{coupon.discount_type === 'fixed' && coupon.multi_currency_discount_value?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted}}",
"props": { "className": "text-xl font-bold text-blue-600 dark:text-blue-400" },
"text": "{{coupon.multi_currency_discount_value?.[_global.preferredCurrency ?? 'KRW']?.formatted}}"
"text": "{{coupon.multi_currency_discount_value?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted}}"
},
{
"comment": "정액 할인 — 폴백(환산 부재 시 기본통화 숫자 + 단위)",
"type": "basic",
"name": "Span",
"if": "{{coupon.discount_type === 'fixed' && !coupon.multi_currency_discount_value?.[_global.preferredCurrency ?? 'KRW']?.formatted}}",
"if": "{{coupon.discount_type === 'fixed' && !coupon.multi_currency_discount_value?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted}}",
"props": { "className": "text-xl font-bold text-blue-600 dark:text-blue-400" },
"text": "{{Number(coupon.discount_value).toLocaleString()}}$t:shop.coupon_download.currency_unit"
}
@@ -179,15 +179,15 @@
"comment": "최소 주문 금액 — 선택 통화 환산 (A1-④, formatted 가 기호 포함)",
"type": "basic",
"name": "P",
"if": "{{coupon.min_order_amount > 0 && coupon.multi_currency_min_order_amount?.[_global.preferredCurrency ?? 'KRW']?.formatted}}",
"if": "{{coupon.min_order_amount > 0 && coupon.multi_currency_min_order_amount?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted}}",
"props": { "className": "text-xs text-gray-500 dark:text-gray-400 mb-1" },
"text": "$t:shop.coupon_download.min_order_amount {{coupon.multi_currency_min_order_amount?.[_global.preferredCurrency ?? 'KRW']?.formatted}}"
"text": "$t:shop.coupon_download.min_order_amount {{coupon.multi_currency_min_order_amount?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted}}"
},
{
"comment": "최소 주문 금액 — 폴백(환산 부재 시)",
"type": "basic",
"name": "P",
"if": "{{coupon.min_order_amount > 0 && !coupon.multi_currency_min_order_amount?.[_global.preferredCurrency ?? 'KRW']?.formatted}}",
"if": "{{coupon.min_order_amount > 0 && !coupon.multi_currency_min_order_amount?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted}}",
"props": { "className": "text-xs text-gray-500 dark:text-gray-400 mb-1" },
"text": "$t:shop.coupon_download.min_order_amount {{Number(coupon.min_order_amount).toLocaleString()}}$t:shop.coupon_download.currency_unit"
},
@@ -15,13 +15,13 @@
"name": "Span",
"if": "{{product.data.discount_rate > 0}}",
"props": { "className": "text-sm text-gray-500 dark:text-gray-400 line-through" },
"text": "{{product.data.multi_currency_list_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? product.data.list_price_formatted}}"
"text": "{{product.data.multi_currency_list_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? product.data.list_price_formatted}}"
},
{
"type": "basic",
"name": "Div",
"props": { "className": "text-2xl font-bold text-gray-900 dark:text-white" },
"text": "{{product.data.multi_currency_selling_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? product.data.selling_price_formatted}}"
"text": "{{product.data.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? product.data.selling_price_formatted}}"
},
{
"type": "basic",
@@ -81,7 +81,7 @@
"value": "{{_local.currentSelection?.[group.id] ?? ''}}",
"placeholder": "{{groupIndex === 0 || _local.currentSelection?.[product.data.option_groups[groupIndex - 1]?.id] ? '$t:shop.select' : '$t:shop.select_upper_option_first'}}",
"disabled": "{{groupIndex > 0 && !_local.currentSelection?.[product.data.option_groups[groupIndex - 1]?.id]}}",
"options": "{{group.values.map(v => ({ value: v.id, label: v.name + (v.additional_price > 0 ? ' (+' + (v.multi_currency_additional_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? v.additional_price_formatted) + ')' : '') }))}}"
"options": "{{group.values.map(v => ({ value: v.id, label: v.name + (v.additional_price > 0 ? ' (+' + (v.multi_currency_additional_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? v.additional_price_formatted) + ')' : '') }))}}"
},
"actions": [
{
@@ -105,7 +105,7 @@
"optionGroups": "{{product.data.option_groups}}",
"currentSelection": "{{_local.currentSelection}}",
"selectedItems": "{{_local.selectedItems ?? []}}",
"preferredCurrency": "{{_global.preferredCurrency ?? 'KRW'}}"
"preferredCurrency": "{{_global.preferredCurrency ?? _global.defaultCurrency}}"
}
}
]
@@ -212,7 +212,7 @@
"itemIndex": "{{itemIndex}}",
"newQuantity": "{{$args[0]}}",
"selectedItems": "{{_local.selectedItems}}",
"preferredCurrency": "{{_global.preferredCurrency ?? 'KRW'}}"
"preferredCurrency": "{{_global.preferredCurrency ?? _global.defaultCurrency}}"
}
}
]
@@ -221,7 +221,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "text-sm font-bold text-gray-900 dark:text-white" },
"text": "{{item.multiCurrencyTotalPrice?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? item.totalPriceFormatted}}"
"text": "{{item.multiCurrencyTotalPrice?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? item.totalPriceFormatted}}"
}
]
}
@@ -276,7 +276,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "text-xl font-bold text-gray-900 dark:text-white" },
"text": "{{(function() { const items = _local.selectedItems ?? []; if (items.length === 0) return product.data.multi_currency_selling_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? product.data.selling_price_formatted; const total = items.reduce((sum, item) => sum + (item.multiCurrencyTotalPrice?.[_global.preferredCurrency ?? 'KRW']?.value ?? item.totalPrice), 0); return _global.formatCurrency(total, _global.preferredCurrency ?? 'KRW'); })()}}"
"text": "{{(function() { const items = _local.selectedItems ?? []; if (items.length === 0) return product.data.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? product.data.selling_price_formatted; const total = items.reduce((sum, item) => sum + (item.multiCurrencyTotalPrice?.[_global.preferredCurrency ?? _global.defaultCurrency]?.value ?? item.totalPrice), 0); return _local.selectedTotalMultiCurrency?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? items[0]?.multiCurrencyTotalPrice?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? String(total); })()}}"
}
]
},
@@ -27,7 +27,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "text-sm text-gray-500 dark:text-gray-400 line-through" },
"text": "{{product.data?.multi_currency_list_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? product.data?.list_price_formatted}}"
"text": "{{product.data?.multi_currency_list_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? product.data?.list_price_formatted}}"
}
]
},
@@ -48,7 +48,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "text-2xl font-bold text-gray-900 dark:text-white" },
"text": "{{product.data?.multi_currency_selling_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? product.data?.selling_price_formatted}}"
"text": "{{product.data?.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? product.data?.selling_price_formatted}}"
}
]
},
@@ -58,7 +58,7 @@
"name": "Div",
"if": "{{!product.data?.discount_rate || product.data?.discount_rate <= 0}}",
"props": { "className": "text-2xl font-bold text-gray-900 dark:text-white" },
"text": "{{product.data?.multi_currency_selling_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? product.data?.selling_price_formatted}}"
"text": "{{product.data?.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? product.data?.selling_price_formatted}}"
}
]
},
@@ -102,7 +102,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "whitespace-nowrap" },
"text": "{{dlCoupon.multi_currency_benefit_formatted?.[_global.preferredCurrency ?? 'KRW'] ?? dlCoupon.benefit_formatted}}"
"text": "{{dlCoupon.multi_currency_benefit_formatted?.[_global.preferredCurrency ?? _global.defaultCurrency] ?? dlCoupon.benefit_formatted}}"
},
{
"type": "basic",
@@ -17,7 +17,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "text-sm text-gray-500 dark:text-gray-400 line-through" },
"text": "{{product.data?.multi_currency_list_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? product.data?.list_price_formatted}}"
"text": "{{product.data?.multi_currency_list_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? product.data?.list_price_formatted}}"
}
]
},
@@ -38,7 +38,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "text-xl font-bold text-gray-900 dark:text-white" },
"text": "{{product.data?.multi_currency_selling_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? product.data?.selling_price_formatted}}"
"text": "{{product.data?.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? product.data?.selling_price_formatted}}"
}
]
},
@@ -48,7 +48,7 @@
"name": "Div",
"if": "{{!product.data?.discount_rate || product.data?.discount_rate <= 0}}",
"props": { "className": "text-xl font-bold text-gray-900 dark:text-white" },
"text": "{{product.data?.multi_currency_selling_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? product.data?.selling_price_formatted}}"
"text": "{{product.data?.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? product.data?.selling_price_formatted}}"
},
{
"comment": "배송 불가 사전 경고 (B8 — 선택 배송국가로 배송 불가, 모바일 가격 영역)",
@@ -91,7 +91,7 @@
"options": "{{product.data?.options}}",
"currentSelection": "{{_local.currentSelection ?? {}}}",
"selectedOptionItems": "{{_local.selectedOptionItems ?? []}}",
"preferredCurrency": "{{_global.preferredCurrency ?? 'KRW'}}",
"preferredCurrency": "{{_global.preferredCurrency ?? _global.defaultCurrency}}",
"additionalOptionGroups": "{{product.data?.additional_options ?? []}}",
"newGroupName": "{{group.name_localized}}",
"newValue": "{{$event.target.value}}"
@@ -264,7 +264,7 @@
"className": "w-full",
"value": "{{item.additionalOptionSelections?.[addGroup.id] ?? ''}}",
"placeholder": "$t:shop.additional_option_placeholder",
"options": "{{[{ value: '', label: $t('shop.additional_option_placeholder') }, ...(addGroup.values ?? []).map(v => ({ value: v.id, label: v.price_adjustment > 0 ? v.name + ' (' + (v.price_adjustment_formatted ?? ('+' + v.price_adjustment.toLocaleString())) + ')' : v.name }))]}}"
"options": "{{[{ value: '', label: $t('shop.additional_option_placeholder') }, ...(addGroup.values ?? []).map(v => ({ value: v.id, label: v.price_adjustment > 0 ? v.name + ' (' + (v.multi_currency_price_adjustment?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? v.price_adjustment_formatted ?? ('+' + v.price_adjustment.toLocaleString())) + ')' : v.name }))]}}"
},
"actions": [
{
@@ -276,7 +276,7 @@
"valueId": "{{$event.target.value}}",
"selectedOptionItems": "{{_local.selectedOptionItems}}",
"additionalOptionGroups": "{{product.data?.additional_options ?? []}}",
"preferredCurrency": "{{_global.preferredCurrency ?? 'KRW'}}"
"preferredCurrency": "{{_global.preferredCurrency ?? _global.defaultCurrency}}"
}
}
]
@@ -316,7 +316,7 @@
"customText": "{{$event.target.value}}",
"selectedOptionItems": "{{_local.selectedOptionItems}}",
"additionalOptionGroups": "{{product.data?.additional_options ?? []}}",
"preferredCurrency": "{{_global.preferredCurrency ?? 'KRW'}}"
"preferredCurrency": "{{_global.preferredCurrency ?? _global.defaultCurrency}}"
}
}
]
@@ -335,7 +335,7 @@
"props": { "className": "flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 mb-2" },
"children": [
{ "type": "basic", "name": "Span", "text": "$t:shop.additional_options_amount" },
{ "type": "basic", "name": "Span", "text": "{{(() => { const c = (_global.availableCurrencies ?? []).find(x => x.is_default); const dp = c?.decimal_places ?? 0; const n = (item.additionalOptionsTotal ?? 0).toLocaleString(undefined, { minimumFractionDigits: dp, maximumFractionDigits: dp }); const s = c?.symbol ?? '₩'; return '+' + ((c?.code === 'KRW' || s === '₩' || s === '원') ? (n + '원') : (s + n)); })()}}" }
{ "comment": "추가금은 기본통화 기준으로 저장되므로 표시통화 환산값(핸들러가 서버 맵에서 계산)을 우선 사용한다. 없으면 기본통화 표기로 폴백", "type": "basic", "name": "Span", "text": "{{(() => { const code = _global.preferredCurrency ?? _global.defaultCurrency; const cur = (_global.availableCurrencies ?? []).find(x => x.code === code) ?? (_global.availableCurrencies ?? []).find(x => x.is_default); const converted = item.additionalOptionsMultiCurrencyTotal?.[code]?.value; const amount = converted ?? (item.additionalOptionsTotal ?? 0); const dp = cur?.decimal_places ?? 0; const n = amount.toLocaleString(undefined, { minimumFractionDigits: dp, maximumFractionDigits: dp }); const s = cur?.symbol ?? '₩'; return '+' + ((cur?.code === 'KRW' || s === '₩' || s === '원') ? (n + '원') : (s + n)); })()}}" }
]
},
{
@@ -361,7 +361,7 @@
"itemIndex": "{{itemIndex}}",
"newQuantity": "{{$args[0]}}",
"selectedOptionItems": "{{_local.selectedOptionItems}}",
"preferredCurrency": "{{_global.preferredCurrency ?? 'KRW'}}"
"preferredCurrency": "{{_global.preferredCurrency ?? _global.defaultCurrency}}"
}
}
]
@@ -370,7 +370,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "text-sm font-bold text-gray-900 dark:text-white" },
"text": "{{item.multiCurrencyTotalPrice?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? item.totalPriceFormatted}}"
"text": "{{item.multiCurrencyTotalPrice?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? item.totalPriceFormatted}}"
}
]
}
@@ -404,7 +404,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "text-xl font-bold text-gray-900 dark:text-white" },
"text": "{{(_local.selectedOptionItems ?? []).length === 0 ? '-' : (_local.selectedTotalMultiCurrency?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? _local.selectedOptionItems[0]?.multiCurrencyTotalPrice?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? _local.selectedOptionItems[0]?.totalPriceFormatted ?? '')}}"
"text": "{{(_local.selectedOptionItems ?? []).length === 0 ? '-' : (_local.selectedTotalMultiCurrency?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? _local.selectedOptionItems[0]?.multiCurrencyTotalPrice?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? _local.selectedOptionItems[0]?.totalPriceFormatted ?? '')}}"
}
]
},
@@ -425,7 +425,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "text-xl font-bold text-gray-900 dark:text-white" },
"text": "{{_local.noOptionTotalMultiCurrency?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? product.data?.multi_currency_selling_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? product.data?.selling_price_formatted ?? ''}}"
"text": "{{_local.noOptionTotalMultiCurrency?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? product.data?.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? product.data?.selling_price_formatted ?? ''}}"
}
]
},
@@ -229,7 +229,7 @@
"props": { "className": "flex justify-between" },
"children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "$t:shop.order_complete.deposit_amount" },
{ "type": "basic", "name": "Span", "props": { "className": "font-bold text-blue-600 dark:text-blue-400 text-lg" }, "text": "{{orderData?.data?.mc_total_amount?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? orderData?.data?.total_amount_formatted}}" }
{ "type": "basic", "name": "Span", "props": { "className": "font-bold text-blue-600 dark:text-blue-400 text-lg" }, "text": "{{orderData?.data?.mc_total_amount?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? orderData?.data?.total_amount_formatted}}" }
]
},
{
@@ -472,7 +472,7 @@
"comment": "단가 x 수량 (주 통화)",
"type": "basic", "name": "P",
"props": { "className": "text-sm text-gray-600 dark:text-gray-400" },
"text": "{{item.mc_unit_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? item.unit_price_formatted}} x {{item.quantity}}"
"text": "{{item.mc_unit_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? item.unit_price_formatted}} x {{item.quantity}}"
},
{
"comment": "단가 보조 통화들",
@@ -481,7 +481,7 @@
"if": "{{Object.keys(item.mc_unit_price ?? {}).length > 1}}",
"props": { "className": "flex flex-wrap gap-x-2" },
"iteration": {
"source": "{{Object.entries(item.mc_unit_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(item.mc_unit_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -513,7 +513,7 @@
"type": "basic",
"name": "Span",
"props": { "className": "font-medium text-gray-900 dark:text-white" },
"text": "{{item.mc_subtotal_price?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? item.subtotal_price_formatted}}"
"text": "{{item.mc_subtotal_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? item.subtotal_price_formatted}}"
},
{
"comment": "소계 보조 통화들",
@@ -522,7 +522,7 @@
"if": "{{Object.keys(item.mc_subtotal_price ?? {}).length > 1}}",
"props": { "className": "flex flex-wrap gap-x-2 justify-end" },
"iteration": {
"source": "{{Object.entries(item.mc_subtotal_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(item.mc_subtotal_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -621,7 +621,7 @@
"comment": "상품 금액",
"type": "basic", "name": "Div", "props": { "className": "flex justify-between mb-2" }, "children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-600 dark:text-gray-400" }, "text": "$t:shop.order_complete.subtotal" },
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-900 dark:text-white" }, "text": "{{orderData?.data?.mc_subtotal_amount?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? orderData?.data?.subtotal_amount_formatted}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-900 dark:text-white" }, "text": "{{orderData?.data?.mc_subtotal_amount?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? orderData?.data?.subtotal_amount_formatted}}" }
]
},
{
@@ -632,7 +632,7 @@
"comment": "Number() 캐스팅 — Order.total_shipping_amount 는 decimal:2 cast 로 '0.00' 문자열이 응답에 올 수 있어 === 0 비교가 false 회귀",
"type": "basic", "name": "Span",
"props": { "className": "{{Number(orderData?.data?.total_shipping_amount ?? 0) === 0 ? 'text-green-600 dark:text-green-400' : 'text-gray-900 dark:text-white'}}" },
"text": "{{Number(orderData?.data?.total_shipping_amount ?? 0) === 0 ? '$t:shop.checkout.free' : (orderData?.data?.mc_total_shipping_amount?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? orderData?.data?.total_shipping_amount_formatted)}}"
"text": "{{Number(orderData?.data?.total_shipping_amount ?? 0) === 0 ? '$t:shop.checkout.free' : (orderData?.data?.mc_total_shipping_amount?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? orderData?.data?.total_shipping_amount_formatted)}}"
}
]
},
@@ -642,7 +642,7 @@
"if": "{{(orderData?.data?.total_discount_amount ?? 0) > 0}}",
"props": { "className": "flex justify-between mb-2 text-red-600 dark:text-red-400" }, "children": [
{ "type": "basic", "name": "Span", "text": "$t:shop.order_complete.discount" },
{ "type": "basic", "name": "Span", "text": "-{{orderData?.data?.mc_total_discount_amount?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? orderData?.data?.total_discount_amount_formatted}}" }
{ "type": "basic", "name": "Span", "text": "-{{orderData?.data?.mc_total_discount_amount?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? orderData?.data?.total_discount_amount_formatted}}" }
]
},
{
@@ -659,7 +659,7 @@
"type": "basic", "name": "Div",
"props": { "className": "flex justify-between items-center pt-2 border-t border-gray-200 dark:border-gray-700 font-bold text-lg" }, "children": [
{ "type": "basic", "name": "Span", "props": { "className": "text-gray-900 dark:text-white" }, "text": "$t:shop.order_complete.total" },
{ "type": "basic", "name": "Span", "props": { "className": "text-blue-600 dark:text-blue-400" }, "text": "{{orderData?.data?.mc_total_amount?.[_global.preferredCurrency ?? 'KRW']?.formatted ?? orderData?.data?.total_amount_formatted}}" }
{ "type": "basic", "name": "Span", "props": { "className": "text-blue-600 dark:text-blue-400" }, "text": "{{orderData?.data?.mc_total_amount?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? orderData?.data?.total_amount_formatted}}" }
]
},
{
@@ -669,7 +669,7 @@
"if": "{{Object.keys(orderData?.data?.mc_total_amount ?? {}).length > 1}}",
"props": { "className": "flex flex-wrap gap-x-2 justify-end mt-1" },
"iteration": {
"source": "{{Object.entries(orderData?.data?.mc_total_amount ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? 'KRW'))}}",
"source": "{{Object.entries(orderData?.data?.mc_total_amount ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}",
"item_var": "currency"
},
"children": [
@@ -27,12 +27,15 @@ import type { EditorAttrs } from '../../types';
* 통화를 바꿔도 카드가 리렌더되지 않았다(상품 리스트/검색/캐러셀 KRW 고정 결함).
* → 전체 객체에서 `.preferredCurrency` 키로 접근 + state.subscribe 로 리렌더한다.
*
* @returns 현재 선호 통화 코드(미설정 시 'KRW')
* 표시 통화 미설정 시에는 쇼핑몰 기본 통화로 폴백한다 — 특정 통화를 못 박으면
* 그 통화를 쓰지 않는 상점에서 multi_currency 조회가 늘 빗나가 기준통화 폴백이 된다.
*
* @returns 현재 선호 통화 코드 (없으면 기본 통화, 그것도 없으면 빈 문자열)
*/
const usePreferredCurrency = (): string => {
const read = (): string => {
const state = (window as any).G7Core?.state?.get?.();
return (state && state.preferredCurrency) || 'KRW';
return (state && (state.preferredCurrency || state.defaultCurrency)) || '';
};
const [currency, setCurrency] = useState<string>(read);
@@ -42,7 +45,7 @@ const usePreferredCurrency = (): string => {
setCurrency(read());
if (typeof subscribe !== 'function') return;
const unsubscribe = subscribe((state: Record<string, any>) => {
setCurrency((state && state.preferredCurrency) || 'KRW');
setCurrency((state && (state.preferredCurrency || state.defaultCurrency)) || '');
});
return typeof unsubscribe === 'function' ? unsubscribe : undefined;
}, []);
@@ -0,0 +1,105 @@
/**
* 통화 포맷 핸들러의 설정 기반 동작 회귀 테스트
*
* 종전 formatCurrencyHandler 는 KRW/USD/JPY/CNY/EUR 5종 고정 표를 두고
* `CURRENCY_CONFIGS[currency] || CURRENCY_CONFIGS.KRW` 로 폴백했다. 운영자가 관리자에서
* 통화를 추가할 수 있으므로(예: GBP), 표에 없는 통화는 값은 그 통화인데 기호·자릿수만
* 원화로 표기되는 상태가 됐다.
*
* 통화 표기는 설정(`language_currency.currencies`)이 정한다는 것을 고정한다.
*/
import { describe, it, expect, afterEach } from 'vitest';
import { formatCurrencyHandler, getCurrencySymbol } from '../formatCurrency';
/**
* 전역 통화 설정과 표시 통화를 심습니다.
*
* @param currencies 설정에 등록된 통화 목록
* @param preferred 표시 통화 코드
*/
function installState(currencies: any[], preferred?: string): any {
const state: Record<string, any> = {
modules: { 'sirsoft-ecommerce': { language_currency: { currencies } } },
};
if (preferred) {
state.preferredCurrency = preferred;
}
(window as any).G7Core = { state: { get: () => state } };
return {
setState: () => {},
getState: (path: string) => {
if (path === '_global.preferredCurrency') return state.preferredCurrency;
if (path === '_global.defaultCurrency') return state.defaultCurrency;
return undefined;
},
} as any;
}
describe('formatCurrencyHandler — 설정 기반 통화 표기', () => {
afterEach(() => {
delete (window as any).G7Core;
});
it('설정에만 있는 통화(GBP)를 원화로 폴백하지 않는다', () => {
const ctx = installState(
[{ code: 'GBP', symbol: '£', decimal_places: 2, is_default: true }],
'GBP'
);
const formatted = formatCurrencyHandler({ value: 1234.5 }, ctx);
expect(formatted).toContain('£');
expect(formatted).not.toContain('₩');
expect(formatted).not.toContain('원');
});
it('설정의 decimal_places 를 따른다', () => {
const ctx = installState(
[{ code: 'GBP', symbol: '£', decimal_places: 2, is_default: true }],
'GBP'
);
expect(formatCurrencyHandler({ value: 1234.5 }, ctx)).toBe('£1,234.50');
});
it('원화는 기존과 동일하게 금액 뒤에 원을 붙인다', () => {
const ctx = installState(
[{ code: 'KRW', symbol: '₩', decimal_places: 0, is_default: true }],
'KRW'
);
expect(formatCurrencyHandler({ value: 10000 }, ctx)).toBe('10,000원');
});
it('표시 통화가 없으면 기본 통화로 표기한다', () => {
const ctx = installState([
{ code: 'JPY', symbol: '¥', decimal_places: 0, is_default: true },
{ code: 'KRW', symbol: '₩', decimal_places: 0 },
]);
const formatted = formatCurrencyHandler({ value: 5000 }, ctx);
expect(formatted).toContain('¥');
expect(formatted).not.toContain('원');
});
it('통화를 판정할 수 없으면 단위를 임의로 붙이지 않는다', () => {
const ctx = installState([]);
const formatted = formatCurrencyHandler({ value: 3000 }, ctx);
expect(formatted).not.toContain('원');
expect(formatted).not.toContain('₩');
});
it('getCurrencySymbol 은 설정의 기호를 우선한다', () => {
installState([{ code: 'CNY', symbol: '元', decimal_places: 2 }]);
expect(getCurrencySymbol('CNY')).toBe('元');
});
});
@@ -0,0 +1,212 @@
/**
* @file productOptionsAdditionalCurrency.test.ts
* @description 추가옵션 추가금의 통화 환산 회귀 테스트
*
* 배경: 추가옵션 추가금은 쇼핑몰 **기본 통화** 기준으로 저장되는데, 총액 재계산이 그 값을
* KRW 로 간주해 표시통화가 KRW 이면 환산 없이 그대로 더했다. 기본통화가 KRW 인 쇼핑몰에서만
* 우연히 맞았고, 기본통화가 JPY 인 쇼핑몰을 KRW 로 보면
* "환산된 상품가 + 환산 안 된 추가금" 이라는 통화가 섞인 합계가 표시됐다.
*
* 서버가 선택지마다 `multi_currency_price_adjustment` 를 내려주므로, 총액은 그 맵의
* 표시통화 값을 써야 한다.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
updateSelectedItemQuantityHandler,
addSelectedItemIfCompleteHandler,
} from '../productOptions';
const mockG7Core = {
state: { get: vi.fn(() => ({})), getLocal: vi.fn(() => ({})), setLocal: vi.fn() },
toast: { show: vi.fn(), warning: vi.fn() },
t: vi.fn((key: string) => key),
};
/**
* 기본통화 JPY / 표시통화 KRW 인 쇼핑몰의 옵션 단가 맵.
* 상품가 10,000 JPY = 95,000 KRW (1,000 JPY = 9,500 KRW 가정).
*/
const unitPriceMap = {
JPY: { value: 10000, formatted: '¥10,000' },
KRW: { value: 95000, formatted: '95,000원' },
};
/**
* 추가금 5,000 JPY = 47,500 KRW 인 추가옵션 그룹.
*/
const additionalOptionGroups = [
{
id: 10,
name: '포장 방식',
is_required: false,
values: [
{
id: 24,
name: '프리미엄 포장',
price_adjustment: 5000,
is_default: false,
multi_currency_price_adjustment: {
JPY: { price: 5000, formatted: '+¥5,000', is_default: true },
KRW: { price: 47500, formatted: '+47,500원', is_default: false },
},
},
],
},
];
const makeItem = (overrides: any = {}) => ({
id: 'opt1',
optionId: 1,
options: {},
optionValues: {},
quantity: 1,
stock: 99,
unitPrice: 10000,
unitPriceFormatted: '¥10,000',
totalPrice: 15000,
totalPriceFormatted: '¥15,000',
multiCurrencyUnitPrice: { ...unitPriceMap },
multiCurrencyTotalPrice: { ...unitPriceMap },
additionalOptionSelections: { 10: 24 },
additionalOptionsTotal: 5000,
additionalOptionsMultiCurrencyTotal: {
JPY: { value: 5000 },
KRW: { value: 47500 },
},
...overrides,
});
describe('추가옵션 추가금 통화 환산 (기본통화 ≠ 표시통화)', () => {
beforeEach(() => {
(window as any).G7Core = mockG7Core;
});
afterEach(() => {
vi.restoreAllMocks();
delete (window as any).G7Core;
});
it('표시통화 KRW 합계에 추가금이 환산되어 더해진다 (기준통화 값이 그대로 섞이지 않는다)', () => {
const setState = vi.fn();
updateSelectedItemQuantityHandler(
{
handler: 'x',
params: {
itemIndex: 0,
newQuantity: 1,
selectedOptionItems: [makeItem()],
preferredCurrency: 'KRW',
additionalOptionGroups,
},
} as any,
{ setState } as any
);
const arg = setState.mock.calls[0][0];
// 기대: 95,000(상품가 환산) + 47,500(추가금 환산) = 142,500
// 수정 전: 95,000 + 5,000(환산 안 된 기준통화 값) = 100,000
expect(arg.selectedTotalMultiCurrency.KRW.value).toBe(142500);
// 기본통화(JPY)는 원값 그대로 10,000 + 5,000 = 15,000
expect(arg.selectedTotalMultiCurrency.JPY.value).toBe(15000);
});
it('수량이 늘면 환산된 추가금도 수량만큼 곱해진다', () => {
const setState = vi.fn();
updateSelectedItemQuantityHandler(
{
handler: 'x',
params: {
itemIndex: 0,
newQuantity: 3,
selectedOptionItems: [makeItem()],
preferredCurrency: 'KRW',
additionalOptionGroups,
},
} as any,
{ setState } as any
);
const arg = setState.mock.calls[0][0];
// (95,000 + 47,500) × 3 = 427,500
expect(arg.selectedTotalMultiCurrency.KRW.value).toBe(427500);
});
it('추가옵션을 선택하지 않으면 상품가만 환산되어 합계가 된다', () => {
const setState = vi.fn();
updateSelectedItemQuantityHandler(
{
handler: 'x',
params: {
itemIndex: 0,
newQuantity: 2,
selectedOptionItems: [
makeItem({
additionalOptionSelections: {},
additionalOptionsTotal: 0,
additionalOptionsMultiCurrencyTotal: undefined,
}),
],
preferredCurrency: 'KRW',
additionalOptionGroups,
},
} as any,
{ setState } as any
);
const arg = setState.mock.calls[0][0];
expect(arg.selectedTotalMultiCurrency.KRW.value).toBe(190000);
});
// 블럭 생성 시 자동 적용되는 기본 선택지는 **필수 그룹** 한정이므로 is_required: true 로 둔다
it('선택 항목 생성 시 통화별 추가금 합계가 항목에 실린다', () => {
const setState = vi.fn();
addSelectedItemIfCompleteHandler(
{
handler: 'x',
params: {
productId: 28,
optionGroups: [{ name: '길이', values: ['1m'] }],
options: [
{
id: 1,
option_values: { 길이: '1m' },
stock_quantity: 10,
is_active: true,
selling_price: 10000,
selling_price_formatted: '¥10,000',
multi_currency_selling_price: {
JPY: { price: 10000, formatted: '¥10,000' },
KRW: { price: 95000, formatted: '95,000원' },
},
},
],
currentSelection: { 길이: '1m' },
selectedOptionItems: [],
preferredCurrency: 'KRW',
additionalOptionGroups: [
{
...additionalOptionGroups[0],
is_required: true,
values: [{ ...additionalOptionGroups[0].values[0], is_default: true }],
},
],
},
} as any,
{ setState } as any
);
const arg = setState.mock.calls[0][0];
const item = arg.selectedOptionItems?.[0];
expect(item).toBeDefined();
expect(item.additionalOptionsMultiCurrencyTotal?.KRW?.value).toBe(47500);
expect(arg.selectedTotalMultiCurrency.KRW.value).toBe(142500);
});
});
@@ -2,6 +2,11 @@
* 통화 포맷팅 핸들러
*
* 숫자 값을 통화 형식 문자열로 변환합니다.
*
* 통화 목록은 운영자가 관리자에서 추가/삭제하므로, 이 핸들러는 통화표를 고정해 두지
* 않고 쇼핑몰 설정(`language_currency.currencies`)의 기호(symbol)와 소수 자릿수
* (decimal_places)를 따릅니다. 고정 통화표를 두면 설정에 없는 통화가 전부 특정 통화로
* 폴백되어(예: GBP 를 ₩ + 0자리로) 값은 맞고 단위만 틀린 금액이 화면에 나갑니다.
*/
import { HandlerContext } from '../types';
@@ -14,71 +19,116 @@ interface FormatCurrencyParams {
interface CurrencyConfig {
code: string;
symbol: string;
locale: string;
decimals: number;
symbol?: string;
decimal_places?: number;
is_default?: boolean;
}
/**
* 통화별 기본 설정
* 설정에 symbol 이 비어 있을 때만 쓰는 기호 폴백.
* 위안화(CNY)는 엔화(¥)와 구분되도록 元 을 사용합니다 (이커머스 모듈과 동일 규칙).
*/
const CURRENCY_CONFIGS: Record<string, CurrencyConfig> = {
KRW: { code: 'KRW', symbol: '₩', locale: 'ko-KR', decimals: 0 },
USD: { code: 'USD', symbol: '$', locale: 'en-US', decimals: 2 },
JPY: { code: 'JPY', symbol: '¥', locale: 'ja-JP', decimals: 0 },
CNY: { code: 'CNY', symbol: '¥', locale: 'zh-CN', decimals: 2 },
EUR: { code: 'EUR', symbol: '€', locale: 'de-DE', decimals: 2 },
const SYMBOL_FALLBACK: Record<string, string> = {
KRW: '₩',
USD: '$',
JPY: '¥',
CNY: '元',
EUR: '€',
GBP: '£',
};
/**
* 설정에 decimal_places 가 없을 때만 쓰는 소수 자릿수 폴백.
*/
const DECIMAL_PLACES_FALLBACK: Record<string, number> = {
KRW: 0,
JPY: 0,
};
/**
* 쇼핑몰 설정에 등록된 통화 목록을 전역 상태에서 읽습니다.
*
* @returns currencies 배열 (읽지 못하면 빈 배열)
*/
function getConfiguredCurrencies(): CurrencyConfig[] {
try {
const state = (window as any).G7Core?.state?.get?.() || {};
const lc = state?.modules?.['sirsoft-ecommerce']?.language_currency;
return Array.isArray(lc?.currencies) ? lc.currencies : [];
} catch {
return [];
}
}
/**
* 숫자 값을 통화 형식으로 포맷팅합니다.
*
* 원화 계열(₩/원)은 금액 뒤에 "원", 그 외는 기호를 앞에 붙입니다
* (백엔드 messages.currency.prefix/suffix 와 동일한 표기 규칙).
*
* @param params.value - 포맷팅할 숫자 값
* @param params.currencyCode - 통화 코드 (KRW, USD, JPY 등)
* @param params.locale - 로케일 (선택, 미지정 시 통화 기본 로케일 사용)
* @param params.currencyCode - 통화 코드 (미지정 시 표시 통화 → 기본 통화 순)
* @param params.locale - 로케일 (숫자 구분자용, 미지정 시 브라우저 로케일)
* @param context - 핸들러 컨텍스트
* @returns 포맷팅된 통화 문자열
*
* @example
* formatCurrencyHandler({ value: 10000, currencyCode: 'KRW' }, context)
* // => "₩10,000" 또는 "10,000원"
*
* formatCurrencyHandler({ value: 99.99, currencyCode: 'USD' }, context)
* // => "$99.99"
* formatCurrencyHandler({ value: 10000, currencyCode: 'KRW' }, context) // => "10,000원"
* formatCurrencyHandler({ value: 99.99, currencyCode: 'USD' }, context) // => "$99.99"
*/
export function formatCurrencyHandler(
params: FormatCurrencyParams,
context: HandlerContext
): string {
const { value, currencyCode, locale: customLocale } = params;
const currency = currencyCode || context.getState('_global.preferredCurrency') || 'KRW';
const currencies = getConfiguredCurrencies();
const config = CURRENCY_CONFIGS[currency] || CURRENCY_CONFIGS.KRW;
const locale = customLocale || config.locale;
const code = currencyCode
|| context.getState('_global.preferredCurrency')
|| context.getState('_global.defaultCurrency')
|| currencies.find((c) => c.is_default)?.code;
try {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: config.code,
minimumFractionDigits: config.decimals,
maximumFractionDigits: config.decimals,
}).format(value);
} catch {
// Intl 지원 안 되는 환경 폴백
const formatted = value.toLocaleString(undefined, {
minimumFractionDigits: config.decimals,
maximumFractionDigits: config.decimals,
});
return `${config.symbol}${formatted}`;
if (!code) {
// 통화를 판정할 수 없으면 단위를 임의로 붙이지 않는다 — 그것이 곧 하드코딩이다.
return value.toLocaleString(customLocale);
}
const config = currencies.find((c) => c.code === code);
const symbol = (config?.symbol && config.symbol.length > 0)
? config.symbol
: (SYMBOL_FALLBACK[code] ?? '');
const decimals = config?.decimal_places ?? DECIMAL_PLACES_FALLBACK[code] ?? 2;
const formatted = value.toLocaleString(customLocale, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
// 기호를 모르면 코드 접미 (백엔드 동일 규칙)
if (!symbol) {
return `${formatted} ${code}`;
}
if (code === 'KRW' || symbol === '₩' || symbol === '원') {
return `${formatted}원`;
}
return `${symbol}${formatted}`;
}
/**
* 통화 심볼만 반환합니다.
*
* 설정의 symbol 을 우선하고, 없으면 폴백 표, 그것도 없으면 통화 코드를 그대로 돌려줍니다.
*
* @param currencyCode - 통화 코드
* @returns 통화 심볼
*/
export function getCurrencySymbol(currencyCode: string): string {
return CURRENCY_CONFIGS[currencyCode]?.symbol || currencyCode;
const config = getConfiguredCurrencies().find((c) => c.code === currencyCode);
return (config?.symbol && config.symbol.length > 0)
? config.symbol
: (SYMBOL_FALLBACK[currencyCode] || currencyCode);
}
@@ -47,7 +47,11 @@ export function getDisplayPriceHandler(
context: HandlerContext
): string {
const { product, priceField, currencyCode } = params;
const preferredCurrency = currencyCode || context.getState('_global.preferredCurrency') || 'KRW';
// 통화를 못 박지 않는다 — 판정 실패 시 아래 *_formatted(기준 통화 표기)로 폴백한다.
const preferredCurrency = currencyCode
|| context.getState('_global.preferredCurrency')
|| context.getState('_global.defaultCurrency')
|| '';
const multiCurrencyField = `multi_currency_${priceField}` as keyof Product;
const multiCurrencyData = product[multiCurrencyField] as Record<string, CurrencyPrice> | undefined;
@@ -14,7 +14,25 @@ const logger = ((window as any).G7Core?.createLogger?.('Handler:Currency')) ?? {
import { HandlerContext } from '../types';
const STORAGE_KEY = 'g7_preferred_currency';
const DEFAULT_CURRENCY = 'KRW';
/**
* 쇼핑몰 설정의 기본 통화를 전역 상태에서 읽습니다.
*
* 특정 통화를 상수로 못 박으면 그 통화를 쓰지 않는 상점에서 표시 통화가 잘못 초기화된다.
*
* @returns 기본 통화 코드 (읽지 못하면 빈 문자열)
*/
function resolveConfiguredDefaultCurrency(): string {
try {
const state = (window as any).G7Core?.state?.get?.() || {};
return state.defaultCurrency
|| state?.modules?.['sirsoft-ecommerce']?.language_currency?.default_currency
|| '';
} catch {
return '';
}
}
interface LoadPreferredCurrencyParams {
defaultCurrency?: string;
@@ -42,7 +60,7 @@ export function loadPreferredCurrencyHandler(
params: LoadPreferredCurrencyParams,
context: HandlerContext
): string {
const { defaultCurrency = DEFAULT_CURRENCY } = params;
const { defaultCurrency = resolveConfiguredDefaultCurrency() } = params;
let currency = defaultCurrency;
@@ -61,7 +61,15 @@ interface ProductOptionRecord {
interface AdditionalOptionValue {
id: number;
name: string;
/** 추가금 (쇼핑몰 **기본 통화** 기준 — KRW 고정이 아니다) */
price_adjustment: number;
/**
* 통화별 추가금 (서버 `multi_currency_price_adjustment`).
*
* 표시통화가 기본통화와 다르면 `price_adjustment` 를 그대로 쓰면 안 된다.
* 상품가는 환산되어 오는데 추가금만 기준통화로 남으면 합계에서 통화가 섞인다.
*/
multi_currency_price_adjustment?: Record<string, { price?: number; value?: number; formatted: string }>;
is_default: boolean;
/** 직접입력 허용 — 이 선택지 선택 시 자유 텍스트 입력칸 노출(입력 필수) */
allow_custom_text?: boolean;
@@ -99,8 +107,10 @@ interface SelectedItem {
additionalOptionSelections?: AdditionalOptionSelections;
/** 블럭별 추가옵션 직접입력 텍스트 (그룹ID → custom_text). allow_custom_text 선택지 한정 */
additionalOptionCustomTexts?: Record<number, string>;
/** 블럭별 추가옵션 추가금 합계 (KRW 기준, 단위당) */
/** 블럭별 추가옵션 추가금 합계 (기본 통화 기준, 단위당) */
additionalOptionsTotal?: number;
/** 블럭별 추가옵션 추가금 합계의 통화별 값 (단위당). 표시통화 합계 계산에 쓴다 */
additionalOptionsMultiCurrencyTotal?: Record<string, { value: number }>;
}
interface AddSelectedItemParams {
@@ -157,68 +167,126 @@ interface ActionDefinition {
}
/**
* 통화별 포맷팅 설정
* 통화 코드별 소수 자릿수 폴백 (설정에 decimal_places 가 없을 때만 사용).
*/
const CURRENCY_CONFIGS: Record<string, { locale: string; decimals: number }> = {
KRW: { locale: 'ko-KR', decimals: 0 },
USD: { locale: 'en-US', decimals: 2 },
JPY: { locale: 'ja-JP', decimals: 0 },
CNY: { locale: 'zh-CN', decimals: 2 },
EUR: { locale: 'de-DE', decimals: 2 },
const DECIMAL_PLACES_FALLBACK: Record<string, number> = {
KRW: 0,
JPY: 0,
};
/**
* 숫자를 통화 형식으로 포맷팅
* 쇼핑몰 설정에 등록된 통화 목록을 전역 상태에서 읽습니다.
*
* @returns currencies 배열 (읽지 못하면 빈 배열)
*/
function formatPrice(amount: number, currencyCode: string = 'KRW'): string {
if (!Number.isFinite(amount)) return '0';
const config = CURRENCY_CONFIGS[currencyCode] || CURRENCY_CONFIGS.KRW;
function getConfiguredCurrencies(): Array<{ code: string; decimal_places?: number; is_default?: boolean }> {
try {
return new Intl.NumberFormat(config.locale, {
const state = (window as any).G7Core?.state?.get?.() || {};
const lc = state?.modules?.['sirsoft-ecommerce']?.language_currency;
return Array.isArray(lc?.currencies) ? lc.currencies : [];
} catch {
return [];
}
}
/**
* 숫자를 통화 형식으로 포맷팅
*
* 통화 목록은 운영자가 관리자에서 추가/삭제하므로 고정 통화표를 두지 않는다. 설정에 없는
* 통화를 특정 통화로 폴백시키면(예: GBP 를 원화 자릿수로) 값은 맞고 단위만 틀린 금액이 나간다.
* 자릿수는 설정의 decimal_places 를 따르고, 기호·표기 규칙은 Intl 이 통화 코드로 판정한다.
*
* @param amount 금액
* @param currencyCode 통화 코드 (미지정 시 설정의 기본 통화)
* @returns 포맷된 금액 문자열
*/
function formatPrice(amount: number, currencyCode?: string): string {
if (!Number.isFinite(amount)) return '0';
const currencies = getConfiguredCurrencies();
const code = currencyCode || currencies.find((c) => c.is_default)?.code;
if (!code) {
// 통화를 판정할 수 없으면 단위를 임의로 붙이지 않는다.
return amount.toLocaleString();
}
const decimals = currencies.find((c) => c.code === code)?.decimal_places
?? DECIMAL_PLACES_FALLBACK[code]
?? 2;
try {
return new Intl.NumberFormat(undefined, {
style: 'currency',
currency: currencyCode,
minimumFractionDigits: config.decimals,
maximumFractionDigits: config.decimals,
currency: code,
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(amount);
} catch {
return amount.toLocaleString() + (currencyCode === 'KRW' ? '원' : '');
return `${amount.toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
})} ${code}`;
}
}
/**
* 다중 통화 총 가격 재계산
*
* 추가옵션 추가금(additionalUnitKrw)이 있으면 KRW 환율 기준으로 환산해 각 통화 단가에 가산한다.
* 추가옵션은 공개 응답에 다통화 컬럼이 없으므로(plan §245) 기본통화(KRW) 단가 비율로 환산한다.
* 추가옵션 추가금은 서버가 통화별로 환산해 내려준 맵(`additionalUnitByCurrency`)에서 읽는다.
*
* 종전에는 추가금을 KRW 로 간주해 표시통화가 KRW 이면 환산 없이 그대로 더했다. 추가금은
* 쇼핑몰 **기본 통화** 기준이라 기본통화가 KRW 인 쇼핑몰에서만 우연히 맞았고, 기본통화가
* 다른 쇼핑몰에서는 "환산된 상품가 + 환산 안 된 추가금" 이라는 통화가 섞인 합계가 나왔다.
*
* 서버 맵이 없는 응답(구버전)에서는 단가 비율로 환산한다. 이때 기준은 **기본 통화**다 —
* 종전처럼 KRW 를 기준으로 잡으면 기본통화가 KRW 가 아닌 쇼핑몰에서 어긋난다. 기본 통화를
* 판정할 수 없는 형태(내부 `{value, formatted}` 맵)면 마지막 수단으로 KRW 를 기준으로 둔다.
*
* @param unitPriceMap 옵션 단가 다통화 맵
* @param quantity 수량
* @param additionalUnitKrw 추가옵션 추가금 합계 (KRW 기준, 단위당). 기본 0.
* @param additionalUnitByCurrency 통화별 추가옵션 추가금 합계 (단위당)
* @param additionalUnitBase 기본 통화 기준 추가금 합계 (단위당, 폴백 환산용)
*/
function recalcMultiCurrencyTotal(
unitPriceMap: Record<string, { value: number; formatted: string }> | undefined,
quantity: number,
additionalUnitKrw: number = 0
additionalUnitByCurrency?: Record<string, { value: number }>,
additionalUnitBase: number = 0
): Record<string, { value: number; formatted: string }> | undefined {
if (!unitPriceMap) return undefined;
// KRW 단가를 환산 기준으로 사용 (옵션의 KRW 단가 대비 타통화 비율)
const krwUnit = (unitPriceMap.KRW as any)?.value ?? (unitPriceMap.KRW as any)?.price ?? 0;
const readAmount = (entry: any): number => entry?.value ?? entry?.price ?? 0;
// 폴백 환산의 기준 통화: is_default 로 표시된 통화 → 없으면 KRW (기존 동작 보존)
let pivotCode: string | undefined;
for (const [code, data] of Object.entries(unitPriceMap)) {
if ((data as any)?.is_default) {
pivotCode = code;
break;
}
}
pivotCode = pivotCode ?? (unitPriceMap.KRW ? 'KRW' : undefined);
const pivotUnit = pivotCode ? readAmount(unitPriceMap[pivotCode]) : 0;
const result: Record<string, { value: number; formatted: string }> = {};
for (const [code, data] of Object.entries(unitPriceMap)) {
// API는 { price, formatted } 형태, 내부는 { value, formatted } 형태 — 둘 다 지원
const unitValue = (data as any)?.value ?? (data as any)?.price ?? 0;
// 추가금 통화 환산: KRW 기준 추가금 × (해당통화 단가 / KRW 단가). KRW 자신은 그대로.
let additionalConverted = 0;
if (additionalUnitKrw > 0) {
if (code === 'KRW') {
additionalConverted = additionalUnitKrw;
} else if (krwUnit > 0) {
additionalConverted = (additionalUnitKrw * unitValue) / krwUnit;
}
const unitValue = readAmount(data);
let additionalValue: number;
if (additionalUnitByCurrency) {
// 서버가 통화별로 환산해 준 값이 있으면 그것이 정답이다
additionalValue = additionalUnitByCurrency[code]?.value ?? 0;
} else if (additionalUnitBase > 0 && code === pivotCode) {
additionalValue = additionalUnitBase;
} else if (additionalUnitBase > 0 && pivotUnit > 0) {
additionalValue = (additionalUnitBase * unitValue) / pivotUnit;
} else {
additionalValue = 0;
}
const total = (unitValue + additionalConverted) * quantity;
const total = (unitValue + additionalValue) * quantity;
result[code] = {
value: total,
formatted: formatPrice(total, code),
@@ -279,6 +347,50 @@ function computeAdditionalOptionsTotal(
return total;
}
/**
* 선택된 추가옵션 추가금의 **통화별** 합계를 계산합니다 (단위당).
*
* 서버가 선택지마다 내려주는 `multi_currency_price_adjustment` 를 통화코드별로 합산한다.
* 그 맵이 없는 선택지(구버전 응답)는 기본통화 값만 있는 셈이므로 합산에서 제외하지 않고
* 통화 구분 없이 더하면 통화가 섞이므로, 맵이 있는 선택지만 통화별 합계에 반영한다.
*
* @param selections 블럭별 추가옵션 선택 (그룹ID → 선택지ID)
* @param groups 추가옵션 카탈로그
* @returns 통화코드 => { value } 합계 맵 (선택 없음/맵 없음이면 undefined)
*/
function computeAdditionalOptionsMultiCurrencyTotal(
selections: AdditionalOptionSelections | undefined,
groups: AdditionalOptionGroup[] | undefined
): Record<string, { value: number }> | undefined {
if (!selections || !groups?.length) return undefined;
const totals: Record<string, number> = {};
let found = false;
for (const group of groups) {
const valueId = selections[group.id];
if (valueId == null) continue;
const value = group.values?.find(v => v.id === Number(valueId));
const map = value?.multi_currency_price_adjustment;
if (!map) continue;
found = true;
for (const [code, entry] of Object.entries(map)) {
const amount = (entry as any)?.value ?? (entry as any)?.price ?? 0;
totals[code] = (totals[code] ?? 0) + amount;
}
}
if (!found) return undefined;
const result: Record<string, { value: number }> = {};
for (const [code, value] of Object.entries(totals)) {
result[code] = { value };
}
return result;
}
/**
* 추가옵션 선택을 백엔드 입력 형식으로 변환한다.
* additional_option_selections: [{ additional_option_id, value_id }]
@@ -486,6 +598,10 @@ export function addSelectedItemIfCompleteHandler(
additionalOptionSelections,
additionalOptionGroups
);
const additionalOptionsMultiCurrencyTotal = computeAdditionalOptionsMultiCurrencyTotal(
additionalOptionSelections,
additionalOptionGroups
);
const blockUnitTotal = unitPrice + additionalOptionsTotal;
@@ -507,10 +623,12 @@ export function addSelectedItemIfCompleteHandler(
multiCurrencyTotalPrice: recalcMultiCurrencyTotal(
matchedOption.multi_currency_selling_price as any,
1,
additionalOptionsMultiCurrencyTotal,
additionalOptionsTotal
),
additionalOptionSelections,
additionalOptionsTotal,
additionalOptionsMultiCurrencyTotal,
};
const nextItems = [...items, newItem];
@@ -553,6 +671,7 @@ export function updateSelectedItemQuantityHandler(
multiCurrencyTotalPrice: recalcMultiCurrencyTotal(
item?.multiCurrencyUnitPrice,
newQuantity,
item?.additionalOptionsMultiCurrencyTotal,
additionalOptionsTotal
),
};
@@ -616,6 +735,10 @@ export function setBlockAdditionalOptionHandler(
}
const additionalOptionsTotal = computeAdditionalOptionsTotal(selections, additionalOptionGroups);
const additionalOptionsMultiCurrencyTotal = computeAdditionalOptionsMultiCurrencyTotal(
selections,
additionalOptionGroups
);
const unitPrice = item?.unitPrice ?? 0;
const quantity = item?.quantity ?? 1;
const totalPrice = (unitPrice + additionalOptionsTotal) * quantity;
@@ -625,11 +748,13 @@ export function setBlockAdditionalOptionHandler(
additionalOptionSelections: selections,
additionalOptionCustomTexts: customTexts,
additionalOptionsTotal,
additionalOptionsMultiCurrencyTotal,
totalPrice,
totalPriceFormatted: formatPrice(totalPrice, preferredCurrency),
multiCurrencyTotalPrice: recalcMultiCurrencyTotal(
item?.multiCurrencyUnitPrice,
quantity,
additionalOptionsMultiCurrencyTotal,
additionalOptionsTotal
),
};