Merge pull request from gnuboard:HeuJung/issue543
HeuJung/issue543
This commit is contained in:
commit
3b831c8523
107 files changed
+2343
-331
No files matched your search
@@ -387,6 +387,22 @@ Icon 은 `<i>` 글리프라 박스 크기가 곧 `font-size` 다. `w-N h-N` 은
|
||||
|
||||
> 상세: [api-resources.md](docs/backend/api-resources.md), [service-repository.md](docs/backend/service-repository.md)
|
||||
|
||||
### 저장값 + 확장 카탈로그 병합 설정의 공개 응답
|
||||
|
||||
설정 항목이 "운영자 저장값 + 확장이 훅으로 등록한 카탈로그" 의 병합으로 만들어지면, 저장값은 남아 있는데 카탈로그에서 항목이 사라지는 상태가 생긴다 — 그 확장을 삭제·비활성화했거나, 확장이 자기 기능 토글을 껐을 때다. 병합부는 이를 고아 항목으로 표시하지만 저장값의 `is_active` 는 참 그대로 남는다.
|
||||
|
||||
| 금지 | 올바른 사용 |
|
||||
|------|------------|
|
||||
| 공개 응답이 저장값 플래그(`is_active`)만 보고 항목을 내보냄 | 카탈로그 소속(`_orphaned`)을 함께 판정 — 공급 확장이 더 이상 제공하지 않는 항목은 공개 응답에서 제거 |
|
||||
| 고아 항목을 관리자 응답에서도 제거 | 관리자 응답은 유지 — 운영자가 확인하고 지워야 할 대상이다 |
|
||||
| 소비 화면(레이아웃 JSON)마다 필터를 넣어 차단 | 공개 API 단일 지점에서 차단 — 화면마다 복제하면 템플릿 하나만 빠져도 같은 결함이 남는다 |
|
||||
| 같은 데이터를 내보내는 공개 엔드포인트가 서로 다른 게터를 사용 | 전 엔드포인트가 같은 공개 게터를 경유 — 한쪽이 raw `getSettings()` 를 쓰면 그 경로만 조용히 뚫린다 |
|
||||
| 항목 제거 후 배열 인덱스를 그대로 둠 | `array_values()` 로 재정렬 — 비연속 키는 JSON 객체로 직렬화되어 화면 반복이 깨진다 |
|
||||
|
||||
이 결함은 예외도 경고도 로그도 남기지 않는다. 이미 제공 불가한 항목이 사용자 화면에서 선택 가능한 상태로 남아 있는 것이 유일한 증상이고, 관리자 화면은 고아 표시로 정상 차단하고 있어 양쪽을 나란히 보지 않으면 드러나지 않는다.
|
||||
|
||||
> 상세: [module-settings.md](docs/extension/module-settings.md) "카탈로그 병합 설정의 공개 응답"
|
||||
|
||||
### 목록 조회 컬럼 프루닝과 지연 조인
|
||||
|
||||
| 금지 | 올바른 사용 |
|
||||
|
||||
+2
-1
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [7.0.6] - 2026-07-19
|
||||
## [7.0.6] - 2026-08-10
|
||||
|
||||
### Security
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
#### 화면 동작
|
||||
|
||||
- 화면에서 선택·클릭 같은 동작에 반응하도록 지정한 항목이 아무 반응도 하지 않던 문제를 수정했습니다. 오류나 경고 없이 조용히 동작하지 않아 원인을 찾기 어려웠습니다. 게시판 글쓰기의 분류 선택이 이에 해당해, 분류를 골라도 글에 반영되지 않았습니다. 함께, 반응은 하되 선택한 값만 비어 저장되던 경우도 수정했습니다.
|
||||
- 화면에서 고른 값이 모듈·플러그인이 제공하는 동작에는 전달되지 않던 문제를 수정했습니다. 값을 저장하는 방식이 두 가지인데 그중 한 가지로 저장하면 화면에는 선택한 것으로 표시되지만 확장 기능은 그 값을 읽지 못했고, 오류나 경고가 남지 않아 화면만 봐서는 알 수 없었습니다. 이제 어느 방식으로 저장하든 같은 값이 전달됩니다.
|
||||
|
||||
#### 레이아웃 코드 편집
|
||||
|
||||
|
||||
@@ -316,7 +316,7 @@ HookManager::doAction('sirsoft-ecommerce.order.after_confirm', $order);
|
||||
| 가상 보호 행 | 코어/번들 확장에 내장된 한국어/영어는 별도 설치 없이 항상 활성/보호 상태로 노출 (수정/제거 차단) |
|
||||
| 보안 | 언어 번역 외의 PHP 실행 코드 포함 시 설치 차단 |
|
||||
|
||||
공식 일본어(ja) 번들 14종(코어 + 주요 모듈/플러그인/템플릿) 이 즉시 사용 가능하며, 인스톨러 4단계에서 모듈/플러그인/템플릿 선택과 종속된 언어팩 카드가 자동 연동되어 함께 설치할 수 있습니다.
|
||||
공식 일본어(ja) 번들 16종(코어 + 주요 모듈/플러그인/템플릿) 이 즉시 사용 가능하며, 인스톨러 4단계에서 모듈/플러그인/템플릿 선택과 종속된 언어팩 카드가 자동 연동되어 함께 설치할 수 있습니다.
|
||||
|
||||
> 상세: [docs/extension/language-packs.md](docs/extension/language-packs.md)
|
||||
|
||||
@@ -392,6 +392,7 @@ cp .env.example .env
|
||||
| **sirsoft-pay_nhnkcp** | NHN KCP 결제 연동 (Standard Pay) |
|
||||
| **sirsoft-tosspayments** | 토스페이먼츠 결제 연동 |
|
||||
| **sirsoft-verification_kginicis** | KG이니시스 본인인증 |
|
||||
| **sirsoft-verification_nhnkcp** | NHN KCP 휴대폰 본인확인 |
|
||||
| **sirsoft-daum_postcode** | 다음 우편번호 검색 |
|
||||
| **sirsoft-marketing** | 마케팅 도구 |
|
||||
| **sirsoft-ckeditor5** | CKEditor 5 에디터 |
|
||||
@@ -423,6 +424,7 @@ cp .env.example .env
|
||||
| **g7-plugin-sirsoft-pay_nhnkcp-ja** | NHN KCP 결제 플러그인 일본어 |
|
||||
| **g7-plugin-sirsoft-tosspayments-ja** | 토스페이먼츠 플러그인 일본어 |
|
||||
| **g7-plugin-sirsoft-verification_kginicis-ja** | KG이니시스 본인인증 플러그인 일본어 |
|
||||
| **g7-plugin-sirsoft-verification_nhnkcp-ja** | NHN KCP 휴대폰 본인확인 플러그인 일본어 |
|
||||
| **g7-template-sirsoft-admin_basic-ja** | 관리자 기본 템플릿 일본어 |
|
||||
| **g7-template-sirsoft-basic-ja** | 사용자 기본 템플릿 일본어 |
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
6. [API 라우트 설정](#6-api-라우트-설정)
|
||||
7. [레이아웃 연동](#7-레이아웃-연동)
|
||||
8. [백엔드에서 설정 조회](#8-백엔드에서-설정-조회)
|
||||
9. [관련 문서](#9-관련-문서)
|
||||
9. [카탈로그 병합 설정의 공개 응답](#9-카탈로그-병합-설정의-공개-응답)
|
||||
10. [관련 문서](#10-관련-문서)
|
||||
|
||||
---
|
||||
|
||||
@@ -527,7 +528,72 @@ $timing = $this->settingsService->getStockDeductionTiming($method);
|
||||
|
||||
---
|
||||
|
||||
## 9. 관련 문서
|
||||
## 9. 카탈로그 병합 설정의 공개 응답
|
||||
|
||||
설정 항목의 목록을 다른 확장이 훅으로 등록하는 경우(결제수단, 배송사, 발급 제공자 등), 저장된 값은 그 카탈로그와 병합되어 응답에 실린다. 운영자가 저장한 값(`is_active`, 정렬 순서 등)과 확장이 제공하는 정의(이름, 아이콘, 지원 범위)가 항목마다 합쳐지는 구조다.
|
||||
|
||||
### 9.1 고아 항목이 생기는 조건
|
||||
|
||||
병합의 두 입력은 생명주기가 다르다. 저장값은 파일에 남지만 카탈로그는 요청 시점에 훅으로 다시 만들어지므로, 다음 상황에서 **저장값만 남고 카탈로그에서는 사라진 항목**이 생긴다.
|
||||
|
||||
- 공급 확장을 삭제했다
|
||||
- 공급 확장을 비활성화했다
|
||||
- 확장이 살아 있지만 자기 기능 토글을 꺼서 해당 항목 등록을 중단했다
|
||||
|
||||
병합부는 이런 항목에 `_orphaned` 표시를 붙여 돌려준다. 이때 저장값의 `is_active` 는 참 그대로 남아 있다 — 운영자가 끈 적이 없기 때문이다.
|
||||
|
||||
### 9.2 관리자 응답과 공개 응답의 처리가 다르다
|
||||
|
||||
| 응답 | 고아 항목 | 이유 |
|
||||
| --- | --- | --- |
|
||||
| 관리자 설정 조회 | **포함** | 운영자가 그 항목이 남아 있음을 확인하고 지울 수 있어야 한다. 화면은 `_orphaned` 를 읽어 편집 불가로 표시한다 |
|
||||
| 공개(소비) 조회 | **제거** | 공급 확장이 더 이상 제공하지 않는 항목이다. 내보내면 사용자가 선택할 수 있게 된다 |
|
||||
|
||||
```php
|
||||
public function getPublicPaymentSettings(): array
|
||||
{
|
||||
$orderSettings = $this->getSettings('order_settings');
|
||||
|
||||
if (isset($orderSettings['payment_methods']) && is_array($orderSettings['payment_methods'])) {
|
||||
$orderSettings['payment_methods'] = array_values(array_filter(
|
||||
$orderSettings['payment_methods'],
|
||||
fn ($method) => ! ($method['_orphaned'] ?? false)
|
||||
));
|
||||
}
|
||||
|
||||
return $orderSettings;
|
||||
}
|
||||
```
|
||||
|
||||
`array_values()` 로 인덱스를 재정렬한다. 중간 항목을 지우면 키가 비연속이 되고, 그 배열은 JSON 객체로 직렬화되어 화면의 반복 렌더가 깨진다.
|
||||
|
||||
### 9.3 차단 지점은 공개 API 한 곳이다
|
||||
|
||||
소비 화면(레이아웃 JSON)의 필터에 맡기지 않는다. 같은 데이터를 그리는 템플릿이 여러 개면 필터가 화면 수만큼 복제되고, 한 곳만 빠져도 같은 결함이 그 화면에서만 남는다.
|
||||
|
||||
같은 데이터를 내보내는 공개 엔드포인트가 둘 이상이면 **전부 같은 공개 게터를 경유**해야 한다. 한쪽이 raw `getSettings()` 를 쓰면 그 경로만 조용히 뚫린다.
|
||||
|
||||
```php
|
||||
// ❌ 엔드포인트마다 다른 게터 — checkout 경로만 고아 항목이 새어 나간다
|
||||
$orderSettings = $this->settingsService->getSettings('order_settings');
|
||||
|
||||
// ✅ 공개 엔드포인트는 모두 공개 게터를 경유
|
||||
$orderSettings = $this->settingsService->getPublicPaymentSettings();
|
||||
```
|
||||
|
||||
### 9.4 진단
|
||||
|
||||
이 결함은 예외도 경고도 로그도 남기지 않는다. 관리자 화면은 고아 표시로 정상 차단하고 있어, 관리자 응답과 공개 응답을 나란히 놓고 항목 수를 비교하기 전에는 드러나지 않는다.
|
||||
|
||||
```bash
|
||||
# 두 응답의 항목 id 목록이 다르면, 그 차이가 고아 항목이다
|
||||
GET /api/modules/{id}/admin/settings # 고아 포함 (정상)
|
||||
GET /api/modules/{id}/settings/payment # 고아 제외 (정상)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 관련 문서
|
||||
|
||||
- [모듈 기초](module-basics.md) - 모듈 구조, AbstractModule
|
||||
- [모듈 라우트](module-routing.md) - API 라우트 규칙
|
||||
|
||||
@@ -4,51 +4,18 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.11] - 2026-08-07
|
||||
|
||||
### Added
|
||||
|
||||
- 개발자 도구 응답 문구 일본어 번역 추가 — 디버그 모드 접근 거부, 연결 확인, 덤프 데이터 없음 안내, 데이터 삭제 완료 안내가 일본어 로케일에서 번역문으로 표시됩니다. 종전에는 사이트 언어와 무관하게 항상 한국어로만 나왔습니다.
|
||||
|
||||
## [1.0.10] - 2026-08-04
|
||||
|
||||
### Added
|
||||
|
||||
- 예약 작업에 등록할 수 없는 Artisan 명령·옵션을 안내하는 문구 일본어 번역 추가 — 왜 등록이 거부됐는지(허용 목록 밖 / 형식 오류 / 허용되지 않은 옵션 / 추가 인자 불가)가 사유별로 표시됩니다.
|
||||
- 언어팩 설치 시 거부 사유를 안내하는 문구 일본어 번역 추가 — 허용되지 않는 파일 형식, 번역 배열이 아닌 PHP 파일(줄 번호 포함), 심볼릭 링크, 그리고 설치 직후 활성화에 필요한 권한 안내입니다.
|
||||
- 환경설정 고급 탭의 목록 상한 항목명(총 건수 집계 상한·페이지 번호 상한) 일본어 번역 추가 — 값이 범위를 벗어났을 때의 안내에 내부 식별자 대신 항목명이 표시됩니다.
|
||||
- 공통 응답 메시지 일본어 번역 추가 (`common.created`·`common.updated`·`common.deleted`) — 생성·수정·삭제 성공 응답이 일본어 로케일에서 번역문으로 표시됩니다.
|
||||
- 본인인증 정책 삭제 제한 안내 메시지 일본어 번역 추가 (`identity.errors.cannot_delete_system_policy`) — 시스템이 선언한 정책은 삭제할 수 없다는 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
|
||||
### Changed
|
||||
|
||||
- PHP 파일 위치 안내 문구를 실제 허용 위치(`backend/{언어코드}/{그룹}.php`)를 알려주도록 다듬었습니다.
|
||||
|
||||
### Removed
|
||||
|
||||
- 더 이상 표시되지 않는 코드 패턴 경고 문구를 제거했습니다 — 같은 상황에서 어느 줄이 문제인지 알려주는 안내로 대체됐습니다.
|
||||
|
||||
## [1.0.9] - 2026-08-01
|
||||
|
||||
### Added
|
||||
|
||||
- 레이아웃 편집기 버전 이력의 "더 보기" 버튼과 변경내용이 너무 클 때의 안내 문구 일본어 번역 추가.
|
||||
- 버전 목록을 한 번에 몇 개까지 가져올지 정하는 값의 입력 안내 문구 일본어 번역 추가.
|
||||
- 통신이 끊겼을 때 표시되는 네트워크 오류 안내 문구 일본어 번역 추가 — 종전에는 내부 식별 문구가 영문 그대로 노출됐습니다.
|
||||
- 목록·검색의 총 건수 정확도 안내 문구(`pagination.*`) 일본어 번역 추가 — 총 건수를 상한까지만 센 경우의 "N건 이상" 표기와 검색어를 좁히라는 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 검색 결과 건수 안내와 검색어 구체화 안내(`search.results_found_at_least`, `search.refine_query_hint`) 일본어 번역 추가.
|
||||
- 페이지 번호 상한 초과 시의 검증 오류 문구(`validation.page_max`) 일본어 번역 추가.
|
||||
|
||||
## [1.0.8] - 2026-08-01
|
||||
|
||||
### Added
|
||||
|
||||
- 검색 인덱스 점검·재생성(`search:index`) 화면 문구 일본어 번역 추가 — 판정 등급(정상/부분적/미반영/판정불가), 점검 결과 표, 재생성 확인·경고 문구가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
|
||||
## [1.0.7] - 2026-07-22
|
||||
## [1.0.5] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
- 자산 파일 주소 방식 확인·전환 명령(`g7:asset-url-mode`) 출력 문구 일본어 번역 추가 (`asset_url_mode.*`) — 현재 방식과 서버 진단 안내, 전환 결과 메시지가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 데이터베이스 계정 설정 오류 안내 화면의 문구 일본어 번역 추가 (`database_credential.*`) — 최고 권한 계정을 사용하거나 사용자명이 비어 있을 때 표시되는 안내와 복구 방법이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 사이트맵 생성이 실패했을 때 표시되는 안내 문구 일본어 번역 추가 (`exceptions.seo.*`) — 사이트맵 파일을 저장하지 못하거나 압축에 실패한 경우의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 사이트맵 재생성 예약·준비 중·생성 실패 안내 문구 일본어 번역 추가 (`seo.sitemap_*`) — 재생성을 예약했을 때, 아직 준비 중일 때, 생성에 실패했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- SEO 설정의 사이트맵 분할 기준·압축·수집기 상한과 SEO/사이트맵 캐시 유지 시간 항목의 검증 안내 문구 일본어 번역 추가 (`validation.settings.*`) — 잘못된 값을 입력했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- SEO 설정의 사이트맵 다국어 대체 링크(hreflang) 항목의 검증 안내 문구 일본어 번역 추가 (`validation.settings.sitemap_hreflang_enabled_boolean`) — 잘못된 값을 입력했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 관리자 사이트맵 수동 재생성 완료·실패 알림 템플릿 일본어 번역 추가 (`sitemap_regenerated`, `sitemap_regenerate_failed`) — 재생성을 실행한 관리자가 받는 앱 내 알림·메일이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 알림 채널 선택 항목의 검증 안내 문구 일본어 번역 추가 (`validation.notification_channel.*`) — 사용할 수 없는 알림 채널을 선택했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 계정이 무기한 잠겼을 때의 로그인 안내와 관리자 잠금 해제 안내, 그리고 해당 활동 기록 문구의 일본어 번역 추가.
|
||||
- 허용되지 않는 파일 형식을 업로드했을 때의 안내 문구 일본어 번역 추가.
|
||||
- 확장 식별자·메뉴 역할·활동 로그 검색 조건의 안내 문구 일본어 번역 추가 — 잘못된 값을 입력했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
@@ -57,25 +24,27 @@
|
||||
- 회원가입 검증 메시지의 휴대폰·전화번호 항목(`validation.auth.mobile.*`, `validation.auth.phone.*`) 일본어 번역 추가 — 회원가입 시 연락처 형식 오류 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 환경설정 저장 시 오류 안내에 나오는 항목 이름의 일본어 번역 추가 — 보안·업로드·SEO·고급·드라이버 등 각 설정 항목이 내부 식별자 대신 일본어 항목명으로 표시됩니다.
|
||||
- 로그인 2단계 인증 안내(인증번호 발송·불일치·요청 만료)와 인증번호 입력 검증 문구의 일본어 번역 추가.
|
||||
- 검색 인덱스 점검·재생성(`search:index`) 화면 문구 일본어 번역 추가 — 판정 등급(정상/부분적/미반영/판정불가), 점검 결과 표, 재생성 확인·경고 문구가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 레이아웃 편집기 버전 이력의 "더 보기" 버튼과 변경내용이 너무 클 때의 안내 문구 일본어 번역 추가.
|
||||
- 버전 목록을 한 번에 몇 개까지 가져올지 정하는 값의 입력 안내 문구 일본어 번역 추가.
|
||||
- 통신이 끊겼을 때 표시되는 네트워크 오류 안내 문구 일본어 번역 추가 — 종전에는 내부 식별 문구가 영문 그대로 노출됐습니다.
|
||||
- 목록·검색의 총 건수 정확도 안내 문구(`pagination.*`) 일본어 번역 추가 — 총 건수를 상한까지만 센 경우의 "N건 이상" 표기와 검색어를 좁히라는 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 검색 결과 건수 안내와 검색어 구체화 안내(`search.results_found_at_least`, `search.refine_query_hint`) 일본어 번역 추가.
|
||||
- 페이지 번호 상한 초과 시의 검증 오류 문구(`validation.page_max`) 일본어 번역 추가.
|
||||
- 예약 작업에 등록할 수 없는 Artisan 명령·옵션을 안내하는 문구 일본어 번역 추가 — 왜 등록이 거부됐는지(허용 목록 밖 / 형식 오류 / 허용되지 않은 옵션 / 추가 인자 불가)가 사유별로 표시됩니다.
|
||||
- 언어팩 설치 시 거부 사유를 안내하는 문구 일본어 번역 추가 — 허용되지 않는 파일 형식, 번역 배열이 아닌 PHP 파일(줄 번호 포함), 심볼릭 링크, 그리고 설치 직후 활성화에 필요한 권한 안내입니다.
|
||||
- 환경설정 고급 탭의 목록 상한 항목명(총 건수 집계 상한·페이지 번호 상한) 일본어 번역 추가 — 값이 범위를 벗어났을 때의 안내에 내부 식별자 대신 항목명이 표시됩니다.
|
||||
- 공통 응답 메시지 일본어 번역 추가 (`common.created`·`common.updated`·`common.deleted`) — 생성·수정·삭제 성공 응답이 일본어 로케일에서 번역문으로 표시됩니다.
|
||||
- 본인인증 정책 삭제 제한 안내 메시지 일본어 번역 추가 (`identity.errors.cannot_delete_system_policy`) — 시스템이 선언한 정책은 삭제할 수 없다는 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 개발자 도구 응답 문구 일본어 번역 추가 — 디버그 모드 접근 거부, 연결 확인, 덤프 데이터 없음 안내, 데이터 삭제 완료 안내가 일본어 로케일에서 번역문으로 표시됩니다. 종전에는 사이트 언어와 무관하게 항상 한국어로만 나왔습니다.
|
||||
|
||||
## [1.0.6] - 2026-07-22
|
||||
### Changed
|
||||
|
||||
### Added
|
||||
- PHP 파일 위치 안내 문구를 실제 허용 위치(`backend/{언어코드}/{그룹}.php`)를 알려주도록 다듬었습니다.
|
||||
|
||||
- 알림 채널 선택 항목의 검증 안내 문구 일본어 번역 추가 (`validation.notification_channel.*`) — 사용할 수 없는 알림 채널을 선택했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
### Removed
|
||||
|
||||
## [1.0.5] - 2026-07-16
|
||||
|
||||
### Added
|
||||
|
||||
- 자산 파일 주소 방식 확인·전환 명령(`g7:asset-url-mode`) 출력 문구 일본어 번역 추가 (`asset_url_mode.*`) — 현재 방식과 서버 진단 안내, 전환 결과 메시지가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
|
||||
- 데이터베이스 계정 설정 오류 안내 화면의 문구 일본어 번역 추가 (`database_credential.*`) — 최고 권한 계정을 사용하거나 사용자명이 비어 있을 때 표시되는 안내와 복구 방법이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 사이트맵 생성이 실패했을 때 표시되는 안내 문구 일본어 번역 추가 (`exceptions.seo.*`) — 사이트맵 파일을 저장하지 못하거나 압축에 실패한 경우의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 사이트맵 재생성 예약·준비 중·생성 실패 안내 문구 일본어 번역 추가 (`seo.sitemap_*`) — 재생성을 예약했을 때, 아직 준비 중일 때, 생성에 실패했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- SEO 설정의 사이트맵 분할 기준·압축·수집기 상한과 SEO/사이트맵 캐시 유지 시간 항목의 검증 안내 문구 일본어 번역 추가 (`validation.settings.*`) — 잘못된 값을 입력했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- SEO 설정의 사이트맵 다국어 대체 링크(hreflang) 항목의 검증 안내 문구 일본어 번역 추가 (`validation.settings.sitemap_hreflang_enabled_boolean`) — 잘못된 값을 입력했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 관리자 사이트맵 수동 재생성 완료·실패 알림 템플릿 일본어 번역 추가 (`sitemap_regenerated`, `sitemap_regenerate_failed`) — 재생성을 실행한 관리자가 받는 앱 내 알림·메일이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 더 이상 표시되지 않는 코드 패턴 경고 문구를 제거했습니다 — 같은 상황에서 어느 줄이 문제인지 알려주는 안내로 대체됐습니다.
|
||||
|
||||
## [1.0.4] - 2026-07-16
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"en": "G7 core Japanese language pack (bundled)",
|
||||
"ja": "G7 コア 日本語 言語パック(バンドル)"
|
||||
},
|
||||
"version": "1.0.11",
|
||||
"version": "1.0.5",
|
||||
"license": "MIT",
|
||||
"scope": "core",
|
||||
"target_identifier": null,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.2] - 2026-08-06
|
||||
## [1.0.2] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -4,16 +4,23 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.1.1] - 2026-08-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- 金額表示から通貨単位(円)の固定表記を削除 — マイレージ・注文金額・一括変更の要約などが、ショップに設定された基準通貨の単位で表示されます。
|
||||
|
||||
## [1.1.0] - 2026-08-06
|
||||
## [1.1.0] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
- 리뷰 작성·답글·일괄처리, 장바구니 선택 항목, 주문서 쿠폰 적용, 클레임 사유 관리, 배송비 검색 조건의 안내 문구 일본어 번역을 추가했습니다. 지금까지 일본어 로케일에서 안내 대신 내부 식별자가 그대로 보이던 화면들이 정상 표시됩니다.
|
||||
- 택배사·상품 공통정보·주문 취소 관련 오류 안내와 상품 문의 답변 상태, 기본 배송 그룹 표시명의 일본어 번역을 추가했습니다.
|
||||
- 장바구니 수량 상한 초과, 정률 적립률 상한 초과, 판매가·가격 하한 안내 문구 일본어 번역 추가.
|
||||
- 쇼핑몰 환경설정·카테고리 이미지·결제 설정 안내 문구 일본어 번역 추가 — 저장/조회 결과 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 검색 프리셋·브랜드·배송사·배송정책 목록 안내 문구 일본어 번역 추가.
|
||||
- 카테고리를 자기 자신이나 하위 카테고리 아래로 옮기려 할 때, 주문에 속하지 않은 옵션을 변경하려 할 때, 상품에 속하지 않은 이미지를 대표로 지정하려 할 때 나오는 안내 문구 일본어 번역 추가.
|
||||
- 상품 문의 처리가 실패했을 때 그 사유를 그대로 보여주는 안내 문구의 일본어 번역을 추가했습니다.
|
||||
- 관리자 상품 목록에서 행을 펼칠 때 나오는 옵션 불러오기 문구(불러오는 중·실패·재시도·등록된 옵션 없음)의 일본어 번역 추가.
|
||||
- 옵션 일괄 변경 확인 화면에서 펼치지 않은 상품을 개수로 묶어 보여주는 문구와 실패 안내의 일본어 번역 추가.
|
||||
- 상품 옵션 조회 요청 값(상품 ID 목록, 옵션 포함 여부) 검증 안내 문구 일본어 번역 추가.
|
||||
- 무통장입금 자동취소 기한을 비운 채 저장할 때 나오는 안내 문구 일본어 번역 추가.
|
||||
- 유효기간이 설정되지 않은 쿠폰을 발급할 때 나오는 안내 문구 일본어 번역 추가.
|
||||
- 마일리지 설정의 통화별 적립 절사 기준(단위·방식) 항목명과 선택지, 허용값 위반 안내 문구 일본어 번역 추가.
|
||||
- 현금영수증 관련 문구(`cash_receipt.*` — 발급 용도, 식별번호 종류, 발급/취소 이력 상태, 배송비 과세 방식, 오류 안내) 일본어 번역 추가 — 현금영수증 발급 화면과 이커머스 환경설정이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 현금영수증 발급 요청의 입력 항목명·검증 안내·처리 결과 문구(`cash_receipt.attributes.*`, `cash_receipt.validation.*`, `cash_receipt.messages.*`)와 발급 불가 사유 안내(`cash_receipt.errors.*`) 일본어 번역 추가 — 잘못된 식별번호를 입력했을 때의 안내와 발급·취소 결과 메시지가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 주문서의 현금영수증 신청 폼 문구(`checkout.cash_receipt.*` — 신청 여부, 발급 용도, 발급 수단, 입력 안내) 일본어 번역 추가 — 무통장입금으로 주문할 때 현금영수증을 신청하는 화면이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
@@ -22,55 +29,17 @@
|
||||
- 현금영수증 자진발급 설정의 국세청 의무발행 업종 안내 링크 문구(`order_settings.cash_receipt.self_issue_mandatory_link`) 일본어 번역 추가 — 자진발급을 켤지 판단할 때 참고하는 국세청 안내 링크가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 카테고리 이미지 처리 결과 안내 문구(`category_images.*` — 업로드·삭제·순서 변경·이미지 없음) 일본어 번역 추가 — 관리자가 카테고리 이미지를 등록·삭제·정렬할 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
|
||||
## [1.0.8] - 2026-08-01
|
||||
|
||||
### Added
|
||||
|
||||
- 관리자 상품 목록에서 행을 펼칠 때 나오는 옵션 불러오기 문구(불러오는 중·실패·재시도·등록된 옵션 없음)의 일본어 번역 추가.
|
||||
- 옵션 일괄 변경 확인 화면에서 펼치지 않은 상품을 개수로 묶어 보여주는 문구와 실패 안내의 일본어 번역 추가.
|
||||
- 상품 옵션 조회 요청 값(상품 ID 목록, 옵션 포함 여부) 검증 안내 문구 일본어 번역 추가.
|
||||
- 무통장입금 자동취소 기한을 비운 채 저장할 때 나오는 안내 문구 일본어 번역 추가.
|
||||
- 유효기간이 설정되지 않은 쿠폰을 발급할 때 나오는 안내 문구 일본어 번역 추가.
|
||||
- 마일리지 설정의 통화별 적립 절사 기준(단위·방식) 항목명과 선택지, 허용값 위반 안내 문구 일본어 번역 추가.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 자동취소 기한 안내가 실제 입력 하한(1일)과 다른 "0일 이상"으로 표시되던 일본어 문구를 수정했습니다.
|
||||
|
||||
## [1.0.7] - 2026-07-31
|
||||
|
||||
### Added
|
||||
|
||||
- 상품 문의 처리가 실패했을 때 그 사유를 그대로 보여주는 안내 문구의 일본어 번역을 추가했습니다.
|
||||
|
||||
### Removed
|
||||
|
||||
- 어디에서도 사용하지 않는 안내 문구 11건의 일본어 번역을 정리했습니다. 한국어 원본이 실제 화면에 쓰이지 않는 위치에 있어 번역만 남아 있던 항목입니다.
|
||||
|
||||
## [1.0.6] - 2026-07-22
|
||||
|
||||
### Added
|
||||
|
||||
- 장바구니 수량 상한 초과, 정률 적립률 상한 초과, 판매가·가격 하한 안내 문구 일본어 번역 추가.
|
||||
- 쇼핑몰 환경설정·카테고리 이미지·결제 설정 안내 문구 일본어 번역 추가 — 저장/조회 결과 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 검색 프리셋·브랜드·배송사·배송정책 목록 안내 문구 일본어 번역 추가.
|
||||
- 카테고리를 자기 자신이나 하위 카테고리 아래로 옮기려 할 때, 주문에 속하지 않은 옵션을 변경하려 할 때, 상품에 속하지 않은 이미지를 대표로 지정하려 할 때 나오는 안내 문구 일본어 번역 추가.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 마일리지 사용 한도 초과 안내에 사용 가능한 최대 금액이 함께 표시되도록 일본어 문구를 갱신했습니다.
|
||||
- 주문 관리자 메모 글자 수 안내가 실제 입력 한도(2000자)와 다른 "1000자"로 표시되던 문제를 수정했습니다.
|
||||
|
||||
## [1.0.5] - 2026-07-19
|
||||
- 자동취소 기한 안내가 실제 입력 하한(1일)과 다른 "0일 이상"으로 표시되던 일본어 문구를 수정했습니다.
|
||||
- 금액 표시에서 통화 단위(円) 고정 표기를 제거했습니다 — 마일리지·주문 금액·일괄 변경 요약 등이 상점에 설정된 기준 통화의 단위로 표시됩니다.
|
||||
|
||||
### Removed
|
||||
|
||||
- 더 이상 사용하지 않는 장바구니 옵션 조합 검증 문구의 일본어 번역을 정리했습니다.
|
||||
|
||||
### Added
|
||||
|
||||
- 리뷰 작성·답글·일괄처리, 장바구니 선택 항목, 주문서 쿠폰 적용, 클레임 사유 관리, 배송비 검색 조건의 안내 문구 일본어 번역을 추가했습니다. 지금까지 일본어 로케일에서 안내 대신 내부 식별자가 그대로 보이던 화면들이 정상 표시됩니다.
|
||||
- 택배사·상품 공통정보·주문 취소 관련 오류 안내와 상품 문의 답변 상태, 기본 배송 그룹 표시명의 일본어 번역을 추가했습니다.
|
||||
- 어디에서도 사용하지 않는 안내 문구 11건의 일본어 번역을 정리했습니다. 한국어 원본이 실제 화면에 쓰이지 않는 위치에 있어 번역만 남아 있던 항목입니다.
|
||||
|
||||
## [1.0.4] - 2026-07-16
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"en": "G7 module (sirsoft-ecommerce) Japanese language pack (bundled)",
|
||||
"ja": "G7 モジュール (sirsoft-ecommerce) 日本語 言語パック(バンドル)"
|
||||
},
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"scope": "module",
|
||||
"target_identifier": "sirsoft-ecommerce",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.2] - 2026-07-22
|
||||
## [1.0.2] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -4,19 +4,14 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.2] - 2026-08-01
|
||||
|
||||
### Added
|
||||
|
||||
- 동의 이력의 출처 "会員登録" / "マイページ一括再同意" 일본어 라벨 추가
|
||||
- 정책 버전 이력 페이지 이동(前へ / 次へ)과 총 건수 표시 일본어 번역 추가
|
||||
- 자동 차단 정책 카드에 보호 범위 한계 안내 문구(운영자가 직접 삽입한 외부 스크립트는 보호 범위 밖) 일본어 번역 추가
|
||||
|
||||
## [1.0.1] - 2026-07-27
|
||||
## [1.0.1] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
- 쿠키 거부 기능 및 마이페이지 「쿠키 동의 관리」 재설계에 따른 신규 문구(상태 배지, 동의·철회 확인 모달, 거부 안내 등) 일본어 번역 추가
|
||||
- 동의 이력의 출처 "会員登録" / "マイページ一括再同意" 일본어 라벨 추가
|
||||
- 정책 버전 이력 페이지 이동(前へ / 次へ)과 총 건수 표시 일본어 번역 추가
|
||||
- 자동 차단 정책 카드에 보호 범위 한계 안내 문구(운영자가 직접 삽입한 외부 스크립트는 보호 범위 밖) 일본어 번역 추가
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"en": "G7 plugin (sirsoft-gdpr) Japanese language pack (bundled)",
|
||||
"ja": "G7 プラグイン (sirsoft-gdpr) 日本語 言語パック(バンドル)"
|
||||
},
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"scope": "plugin",
|
||||
"target_identifier": "sirsoft-gdpr",
|
||||
|
||||
@@ -4,40 +4,26 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.1.2] - 2026-08-07
|
||||
## [1.1.0] - 2026-08-10
|
||||
|
||||
### Fixed
|
||||
### Added
|
||||
|
||||
- 返金金額の案内から通貨単位(円)の固定表記を削除 — 決済通貨の単位で表示されます。
|
||||
|
||||
## [1.1.1] - 2026-08-07
|
||||
- 해외결제(CBT) 연결 진단 결과 안내의 일본어 번역을 추가했습니다. 일본어 로케일에서 안내 대신 내부 식별자가 보이던 문제가 해소됩니다.
|
||||
- 해외결제(CBT) 편의점·가상계좌 입금 시뮬레이션 안내 문구 일본어 번역 추가.
|
||||
- 현금영수증 발급사 이름(`cash_receipt.provider_name`) 일본어 번역 추가.
|
||||
|
||||
### Changed
|
||||
|
||||
- 결제 성공·실패 리다이렉트 주소 안내 문구를 갱신했습니다 — 상점 주소 설정을 따라 자동으로 채워지는 `{shopBase}` 자리표시자 설명으로 바뀌었습니다.
|
||||
|
||||
## [1.1.0] - 2026-08-06
|
||||
### Fixed
|
||||
|
||||
### Added
|
||||
|
||||
- 현금영수증 발급사 이름(`cash_receipt.provider_name`) 일본어 번역 추가.
|
||||
- 환불 금액 안내에서 통화 단위(円) 고정 표기를 제거했습니다 — 결제 통화의 단위로 표시됩니다.
|
||||
|
||||
### Removed
|
||||
|
||||
- KG이니시스 전용 현금영수증 발행 화면이 제거되면서 함께 쓰이지 않게 된 발행 오류 안내 3종의 번역을 정리했습니다.
|
||||
|
||||
## [1.0.3] - 2026-07-22
|
||||
|
||||
### Added
|
||||
|
||||
- 해외결제(CBT) 편의점·가상계좌 입금 시뮬레이션 안내 문구 일본어 번역 추가.
|
||||
|
||||
## [1.0.2] - 2026-07-22
|
||||
|
||||
### Added
|
||||
|
||||
- 해외결제(CBT) 연결 진단 결과 안내의 일본어 번역을 추가했습니다. 일본어 로케일에서 안내 대신 내부 식별자가 보이던 문제가 해소됩니다.
|
||||
|
||||
## [1.0.1] - 2026-07-16
|
||||
|
||||
### Added
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"en": "G7 plugin (sirsoft-pay_kginicis) Japanese language pack (bundled)",
|
||||
"ja": "G7 プラグイン (sirsoft-pay_kginicis) 日本語 言語パック(バンドル)"
|
||||
},
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"scope": "plugin",
|
||||
"target_identifier": "sirsoft-pay_kginicis",
|
||||
|
||||
@@ -4,18 +4,16 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.3] - 2026-08-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- 返金金額の案内から通貨単位(円)の固定表記を削除 — 決済通貨の単位で表示されます。
|
||||
|
||||
## [1.0.2] - 2026-08-07
|
||||
## [1.0.2] - 2026-08-10
|
||||
|
||||
### Changed
|
||||
|
||||
- 결제 성공·실패 리다이렉트 주소 안내 문구를 갱신했습니다 — 상점 주소 설정을 따라 자동으로 채워지는 `{shopBase}` 자리표시자 설명으로 바뀌었습니다.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 환불 금액 안내에서 통화 단위(円) 고정 표기를 제거했습니다 — 결제 통화의 단위로 표시됩니다.
|
||||
|
||||
## [1.0.1] - 2026-07-16
|
||||
|
||||
### Added
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"en": "G7 plugin (sirsoft-pay_nhnkcp) Japanese language pack (bundled)",
|
||||
"ja": "G7 プラグイン (sirsoft-pay_nhnkcp) 日本語 言語パック(バンドル)"
|
||||
},
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"scope": "plugin",
|
||||
"target_identifier": "sirsoft-pay_nhnkcp",
|
||||
|
||||
@@ -4,18 +4,16 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.3] - 2026-08-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- 返金金額の案内から通貨単位(円)の固定表記を削除 — 決済通貨の単位で表示されます。
|
||||
|
||||
## [1.0.2] - 2026-08-07
|
||||
## [1.0.2] - 2026-08-10
|
||||
|
||||
### Changed
|
||||
|
||||
- 결제 성공·실패 리다이렉트 주소 안내 문구를 갱신했습니다 — 상점 주소 설정을 따라 자동으로 채워지는 `{shopBase}` 자리표시자 설명으로 바뀌었습니다.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 환불 금액 안내에서 통화 단위(円) 고정 표기를 제거했습니다 — 결제 통화의 단위로 표시됩니다.
|
||||
|
||||
## [1.0.1] - 2026-07-16
|
||||
|
||||
### Added
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"en": "G7 plugin (sirsoft-pay_nicepayments) Japanese language pack (bundled)",
|
||||
"ja": "G7 プラグイン (sirsoft-pay_nicepayments) 日本語 言語パック(バンドル)"
|
||||
},
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"scope": "plugin",
|
||||
"target_identifier": "sirsoft-pay_nicepayments",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.1] - 2026-08-07
|
||||
## [1.0.1] - 2026-08-10
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.2] - 2026-07-29
|
||||
## [1.0.2] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.0] - 2026-07-29
|
||||
## [1.0.0] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -4,21 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.6] - 2026-08-05
|
||||
|
||||
### Added
|
||||
|
||||
- 레이아웃 버전 이력의 「이전 버전 더 보기」 버튼 문구 일본어 번역 추가 — 기본 표시 개수를 넘는 오래된 버전을 여는 버튼이 일본어 로케일에서도 정상 표기됩니다.
|
||||
|
||||
## [1.0.5] - 2026-08-01
|
||||
|
||||
### Added
|
||||
|
||||
- 역할 관리 목록의 「권한 수」 열 제목 일본어 번역 추가.
|
||||
- 확장 업데이트 모달의 「검색 인덱스 재생성」 선택 항목 문구 일본어 번역 추가 — 항목 이름과 설명(재생성 시 인덱스 잠금·재색인 안내)이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 관리자 목록의 총 건수 정확도 표기와 고급 환경설정의 목록 한계값 항목 문구 일본어 번역 추가 — 총 건수를 상한까지만 센 경우의 "N건 이상" 표기와 상한 설정 항목이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
|
||||
## [1.0.4] - 2026-07-17
|
||||
## [1.0.4] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
@@ -29,12 +15,15 @@
|
||||
- 언어팩 관리 목록의 "파일 없음" 배지와 다시 설치 버튼 문구 일본어 번역 추가 (`language_packs.badge.files_missing`, `language_packs.actions.reinstall`).
|
||||
- 일반 설정의 에셋 파일 서빙 방식 항목 문구 일본어 번역 추가 (`settings.general.asset_url_mode*`) — 항목 이름과 설명, 선택지, 자동 감지 결과(확장자를 가로채는 서버 규칙 안내 포함)가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 대시보드의 에셋 파일 서빙 방식 불일치 안내 문구 일본어 번역 추가 (`dashboard.asset_url_mode_drift_*`) — 안내 제목과 설명, 환경설정 이동 문구가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
|
||||
- 환경설정 > 정보 화면에 추가된 OPcache 상태 항목과 비활성 안내 문구 일본어 번역 추가 — 상태 표시와 성능 저하 안내가 일본어 로케일에서도 올바르게 표시됩니다. 비활성 상태 문구는 성능 저하의 체감 정도가 드러나도록 보강했습니다.
|
||||
- 환경설정 > SEO 화면에 추가된 Sitemap 분할 기준(파일당 URL 수)·Sitemap 압축 항목의 이름·설명 일본어 번역 추가 — 새 항목이 일본어 로케일에서도 올바르게 표시됩니다.
|
||||
- 환경설정 > SEO 화면에 추가된 Sitemap 다국어 대체 링크(hreflang) 항목의 이름·설명 일본어 번역 추가 — 새 항목이 일본어 로케일에서도 올바르게 표시됩니다.
|
||||
- 고급 설정에 추가된 Sitemap 캐시 유지 시간 항목의 이름·설명 일본어 번역 추가 — 새 항목이 일본어 로케일에서도 올바르게 표시됩니다.
|
||||
- 환경설정 > SEO 화면의 Sitemap 생성 진행 상황 표시(대기·생성 중·파일 작성 중·완료·실패 단계, 현재 처리 항목, 누적 URL 수, 실시간/주기 확인 안내) 일본어 번역 추가 — 진행 상황 화면이 일본어 로케일에서도 올바르게 표시됩니다.
|
||||
- 역할 관리 목록의 「권한 수」 열 제목 일본어 번역 추가.
|
||||
- 확장 업데이트 모달의 「검색 인덱스 재생성」 선택 항목 문구 일본어 번역 추가 — 항목 이름과 설명(재생성 시 인덱스 잠금·재색인 안내)이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 관리자 목록의 총 건수 정확도 표기와 고급 환경설정의 목록 한계값 항목 문구 일본어 번역 추가 — 총 건수를 상한까지만 센 경우의 "N건 이상" 표기와 상한 설정 항목이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 레이아웃 버전 이력의 「이전 버전 더 보기」 버튼 문구 일본어 번역 추가 — 기본 표시 개수를 넘는 오래된 버전을 여는 버튼이 일본어 로케일에서도 정상 표기됩니다.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"en": "G7 template (sirsoft-admin_basic) Japanese language pack (bundled)",
|
||||
"ja": "G7 テンプレート (sirsoft-admin_basic) 日本語 言語パック(バンドル)"
|
||||
},
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.4",
|
||||
"license": "MIT",
|
||||
"scope": "template",
|
||||
"target_identifier": "sirsoft-admin_basic",
|
||||
|
||||
@@ -4,19 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.1.1] - 2026-08-07
|
||||
|
||||
### Changed
|
||||
|
||||
- 화면 편집기 상품 상세의 상태 미리보기 라벨(`editor.state.out_of_stock`, `editor.state.sold_out`)을 서로 구분되도록 정정했습니다. 두 상태 모두 "品切れ" 로 표시되어 목록에서 어느 쪽인지 구별할 수 없었습니다.
|
||||
|
||||
## [1.1.0] - 2026-08-06
|
||||
|
||||
### Added
|
||||
|
||||
- 주문서의 환불 계좌 입력란 문구(`shop.checkout.refund_bank_*` — 안내 제목, 은행 선택, 계좌번호·예금주 입력 안내, 부분 입력 주의) 일본어 번역 추가 — 무통장입금·가상계좌로 주문할 때 환불받을 계좌를 남기는 영역이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
|
||||
## [1.0.2] - 2026-07-22
|
||||
## [1.1.0] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
@@ -24,6 +12,11 @@
|
||||
- 모달 닫기 버튼의 안내 라벨(`common.close_modal`) 일본어 번역 추가 — 화면 낭독기 사용자에게 버튼 용도가 일본어로 안내됩니다.
|
||||
- 검색 결과 건수 표기와 검색어 구체화 안내(`search.result_count_suffix_at_least`, `search.refine_query_hint`) 일본어 번역 추가 — 총 건수를 상한까지만 센 경우의 "N건 이상" 표기가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 편집기 화면 문구(`editor.*`) 일본어 번역 보강.
|
||||
- 주문서의 환불 계좌 입력란 문구(`shop.checkout.refund_bank_*` — 안내 제목, 은행 선택, 계좌번호·예금주 입력 안내, 부분 입력 주의) 일본어 번역 추가 — 무통장입금·가상계좌로 주문할 때 환불받을 계좌를 남기는 영역이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
|
||||
### Changed
|
||||
|
||||
- 화면 편집기 상품 상세의 상태 미리보기 라벨(`editor.state.out_of_stock`, `editor.state.sold_out`)을 서로 구분되도록 정정했습니다. 두 상태 모두 "品切れ" 로 표시되어 목록에서 어느 쪽인지 구별할 수 없었습니다.
|
||||
|
||||
## [1.0.1] - 2026-07-08
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"en": "G7 template (sirsoft-basic) Japanese language pack (bundled)",
|
||||
"ja": "G7 テンプレート (sirsoft-basic) 日本語 言語パック(バンドル)"
|
||||
},
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"scope": "template",
|
||||
"target_identifier": "sirsoft-basic",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [0.1.1] - 2026-07-31
|
||||
## [0.1.1] - 2026-08-10
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.3] - 2026-07-16
|
||||
## [1.0.3] - 2026-08-10
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
* 단위/렌더링:
|
||||
* - templates/_bundled/sirsoft-basic/src/__tests__/layouts/board-post-navigation-list-return.test.tsx
|
||||
* 가 실 `_navigation.json` 의 액션 params 로 mergeQuery 왕복을 고정
|
||||
* - .claude/scripts/audit/rules/layout-list-context-navigate-merge-query.cjs 가 전 저장소 재발 차단
|
||||
* - 저장소 전역 정적 검사가 목록 클러스터 내 navigate 의 mergeQuery 누락을 재발 차단
|
||||
* 이 spec 은 브라우저 수준(실제 URL + 목록 API 요청 파라미터)을 담당한다.
|
||||
*
|
||||
* @scenario board-list-context-round-trip
|
||||
|
||||
@@ -35,7 +35,6 @@ test_files:
|
||||
- modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/admin-list-context-round-trip.test.ts
|
||||
- modules/_bundled/sirsoft-page/resources/js/__tests__/layouts/admin-list-context-round-trip.test.ts
|
||||
- templates/_bundled/sirsoft-admin_basic/src/__tests__/layouts/admin-list-context-round-trip.test.ts
|
||||
- .claude/scripts/audit/__tests__/layout-list-context-navigate-merge-query.test.cjs
|
||||
|
||||
validation:
|
||||
note: |
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.1.0] - 2026-08-06
|
||||
## [1.1.0] - 2026-08-10
|
||||
|
||||
### Security
|
||||
|
||||
@@ -97,6 +97,11 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- 기본 통화를 바꾼 뒤 통화 설정을 저장한 상점에서, 결제창에 청구되는 금액이 주문서에 표시된 금액과 다르게 넘어가던 문제를 수정했습니다. 이전 기본 통화가 기본 통화 표시를 계속 유지해 상점에 기본 통화가 둘로 인식됐고, 그 통화의 환율이 1로 간주되어 환산 없이 청구됐습니다. 예를 들어 기본 통화를 엔화로 바꾼 상점에서 23,000엔 주문이 결제창에는 230원으로 넘어갔습니다. 이제 환율을 입력하지 않은 통화로는 결제가 진행되지 않고 미지원 통화로 안내됩니다.
|
||||
- 같은 화면에서 상품별 금액과 총액의 다른 통화 표기가 서로 다른 기준으로 환산되던 문제를 함께 수정했습니다. 위 문제와 원인이 같으며, 주문서의 상품 금액과 총 결제금액의 외화 표기가 어긋나 보였습니다.
|
||||
- 결제대행사 플러그인이 더 이상 제공하지 않는 결제수단이 주문서에 계속 노출되던 문제를 수정했습니다. 예를 들어 토스페이먼츠의 주문서형 결제를 끄면 관리자 환경설정에서는 해당 결제수단이 즉시 사용 불가로 바뀌는데, 주문서에서는 그대로 선택할 수 있었습니다. 플러그인을 삭제하거나 비활성화한 경우에도 동일했습니다. 이제 관리자 화면과 주문서가 같은 기준으로 판단합니다.
|
||||
- 이커머스 환경설정 주문설정의 결제수단 목록에서, 결제대행사를 아직 고르지 않은 줄의 선택 상자가 지나치게 좁게 표시되던 문제를 수정했습니다. 선택 상자 폭이 선택된 값의 글자 수를 따라가 줄마다 항목 위치도 어긋나 있었습니다. 이제 결제대행사·재고 차감 시점·마일리지 차감 시점이 모든 줄에서 같은 폭으로 정렬됩니다.
|
||||
- 같은 목록에서 더 이상 사용할 수 없는 결제수단(공급 플러그인을 지웠거나 끈 경우) 줄에 "마일리지 차감 시점" 항목만 남아 표시되던 문제를 수정했습니다. 다른 설정 항목은 모두 감춰지는데 이 항목만 남아 엉뚱한 위치에 보였고, 마일리지 기능을 사용하는 상점에서는 이미 쓸 수 없는 결제수단의 값을 바꿔 저장할 수도 있었습니다. 화면을 좁게 볼 때는 정상적으로 감춰지고 있어 넓은 화면에서만 나타났습니다.
|
||||
- 기본 통화가 원화가 아닌 상점에서 금액에 원화 단위가 붙던 문제를 수정했습니다. 마일리지 적립·사용·복원·소멸 내역, 결제금액 불일치 안내, 상품 일괄수정 요약, 할인코드 안내가 이제 상점에 설정된 통화의 단위로 표시됩니다.
|
||||
- 통합 검색 결과의 상품 가격이 상점 기본 통화가 아닌 원화 단위로 표기되던 문제를 수정했습니다.
|
||||
- 관리자에게 발송되는 신규 주문 알림의 금액이 구매자 알림과 달리 원화로 고정되던 문제를 수정했습니다. 이제 두 알림 모두 결제 통화로 표시됩니다.
|
||||
|
||||
@@ -55,7 +55,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
| available_pg_providers | array | `[{"id":"kginicis","name_key":"sirsoft-pay_kginicis::provi…` | 설치된 PG 플러그인이 훅으로 등록한 PG 제공자 목록 (id·name_key·지원 결제수단) |
|
||||
| available_cash_receipt_providers | array | `[]` | 설치된 플러그인이 훅으로 등록한 현금영수증 발급 제공자 목록 (id·name_key — 미등록 시 빈 배열이며 신청 폼이 노출되지 않음) |
|
||||
| abilities | object | `{"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
|
||||
| _meta | object | `{"limits":{"auto_cancel_days_min":1,"auto_cancel_days_max…` | <!-- TODO: 설명 --> |
|
||||
| _meta | object | `{"limits":{"auto_cancel_days_min":1,"auto_cancel_days_max…` | 설정 화면 전용 메타. `limits` 는 숫자 입력의 경계값 맵(`config('sirsoft-ecommerce.limits')`)으로, 저장 규칙(FormRequest)과 같은 출처다 — 화면이 리터럴 경계를 들면 규칙 변경을 따라가지 못해 "화면은 받는데 저장은 422" 가 되므로 입력 min/max 는 이 값을 바인딩한다 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1447,11 +1447,37 @@ Authorization: Bearer {YOUR_TOKEN}
|
||||
|
||||
**응답 필드** (`data` 내부)
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
|
||||
_단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| category | string | `"basic_info"` | 조회한 설정 카테고리 (path 파라미터를 그대로 반향) |
|
||||
| settings | object | `{"shop_name":"","route_path":"shop","no_route":false}` | 해당 카테고리의 설정값. 구조는 카테고리마다 다르며 index 응답의 동명 최상위 키와 동일하다 (`order_settings` 조회 시 결제수단은 병합된 목록 — 공급 확장이 빠진 고아 항목 포함) |
|
||||
| abilities | object | `{"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update — 권한 맵 기반) |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "설정을 조회했습니다.",
|
||||
"data": {
|
||||
"category": "basic_info",
|
||||
"settings": {
|
||||
"shop_name": "",
|
||||
"route_path": "shop",
|
||||
"no_route": false
|
||||
},
|
||||
"abilities": {
|
||||
"can_update": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**에러 응답**
|
||||
|
||||
@@ -1491,7 +1517,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| shipping | object | `{"default_country":"KR","available_countries":[{"code":"K…` | 체크아웃용 배송 설정 (기본 국가·배송 가능 국가·무료배송·배송유형 등) |
|
||||
| order_settings | object | `{"default_pg_provider":null,"cash_receipt_provider":"toss…` | 체크아웃용 주문/결제 설정 (기본 PG·활성 결제수단·무통장 계좌 등) |
|
||||
| order_settings | object | `{"default_pg_provider":null,"cash_receipt_provider":"toss…` | 체크아웃용 주문/결제 설정 (기본 PG·활성 결제수단·무통장 계좌 등). `payment_methods` 는 현재 제공 가능한 결제수단만 포함하며, 공급 확장이 더 이상 제공하지 않는 결제수단(관리자 화면의 고아 항목)은 `is_active` 가 참이어도 제외된다 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
@@ -1626,7 +1652,7 @@ _단건 응답: `data` 객체의 필드._
|
||||
|
||||
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|
||||
| --- | --- | --- | --- |
|
||||
| order_settings | object | `{"default_pg_provider":null,"cash_receipt_provider":"toss…` | 공개 가능한 결제 설정 (활성 결제수단·무통장 은행명 매핑 포함, 민감 정보 제외) |
|
||||
| order_settings | object | `{"default_pg_provider":null,"cash_receipt_provider":"toss…` | 공개 가능한 결제 설정 (활성 결제수단·무통장 은행명 매핑 포함, 민감 정보 제외). `payment_methods` 는 현재 제공 가능한 결제수단만 포함하며, 공급 확장이 더 이상 제공하지 않는 결제수단(관리자 화면의 고아 항목)은 `is_active` 가 참이어도 제외된다 |
|
||||
|
||||
**응답 예시**
|
||||
|
||||
|
||||
+156
@@ -9,6 +9,9 @@
|
||||
* - 폼 바인딩 및 핸들러 검증
|
||||
* - 다국어 키 검증
|
||||
*
|
||||
* @scenario extension_payment_method method_kind=extension × capability_declared=declared × capability=pg_locked
|
||||
* @effects admin_shows_pg_locked_badge, admin_hides_pg_select_for_locked, admin_shows_pg_select_for_unlocked
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
@@ -426,6 +429,74 @@ describe('결제수단 Sortable 리스트 구조 검증 (_payment_methods_list.j
|
||||
expect(action.params['form.order_settings.payment_methods']).toContain('is_active');
|
||||
});
|
||||
|
||||
// ─── 필드 열 고정폭 (PG 셀렉트 폭 붕괴 회귀 차단) ───
|
||||
// 배경: Select 는 커스텀 드롭다운이라 className 이 내부 Button 에 붙는데, 컴포넌트
|
||||
// 기본 클래스의 w-full 이 CSS 출력 순서상 w-N 보다 뒤라 셀렉트에 직접 준 폭 토큰은
|
||||
// 무효가 된다(같은 특이도 → 후순위 승리). 그 결과 폭이 내용 폭을 따라가, PG 미선택
|
||||
// 상태('---')에서 74px 로 붕괴하고 선택값 길이에 따라 행마다 열 위치가 어긋났다.
|
||||
// 폭은 열 컨테이너가 책임진다.
|
||||
const FIELD_COLUMN_LABELS = [
|
||||
'payment_methods.pg_provider',
|
||||
'payment_methods.stock_deduction_timing',
|
||||
'payment_methods.mileage_deduction_timing',
|
||||
];
|
||||
|
||||
/** 라벨 i18n 키로 필드 열 컨테이너를 찾는다. */
|
||||
const findFieldColumn = (labelKey: string) =>
|
||||
findFirst(tpl, (n: any) =>
|
||||
typeof n?.props?.className === 'string'
|
||||
&& n.props.className.includes('row-stack')
|
||||
&& Array.isArray(n?.children)
|
||||
&& n.children.some((c: any) => typeof c?.text === 'string' && c.text.includes(labelKey)),
|
||||
);
|
||||
|
||||
it.each(FIELD_COLUMN_LABELS)('필드 열(%s)이 고정폭을 가져야 한다', (labelKey) => {
|
||||
const column = findFieldColumn(labelKey);
|
||||
expect(column).not.toBeNull();
|
||||
expect(
|
||||
column.props.className,
|
||||
`${labelKey} 열에 고정폭이 없으면 셀렉트 폭이 내용 폭을 따라가 미선택 상태에서 붕괴한다`,
|
||||
).toMatch(/\bw-\d+\b/);
|
||||
});
|
||||
|
||||
it('세 필드 열이 동일한 폭 토큰을 사용해야 한다 (행 간 열 정렬)', () => {
|
||||
const widths = FIELD_COLUMN_LABELS.map((labelKey) => {
|
||||
const column = findFieldColumn(labelKey);
|
||||
return (column.props.className.match(/\bw-\d+\b/) ?? [])[0];
|
||||
});
|
||||
expect(new Set(widths).size, `열 폭이 서로 다르면 행 간 열이 어긋난다: ${widths.join(', ')}`).toBe(1);
|
||||
});
|
||||
|
||||
it('필드 열이 축소 가능해야 한다 (좁은 폭에서 행 넘침 차단)', () => {
|
||||
// shrink-0 을 붙이면 고정폭 3열(총 432px)이 줄어들지 않아 좁은 뷰포트에서
|
||||
// 이름 열이 0 까지 붕괴하고도 행이 넘친다 (1100px 실측 17px 초과).
|
||||
for (const labelKey of FIELD_COLUMN_LABELS) {
|
||||
const column = findFieldColumn(labelKey);
|
||||
expect(column.props.className, `${labelKey} 열`).not.toContain('shrink-0');
|
||||
}
|
||||
});
|
||||
|
||||
it('필드 열의 Select 에는 폭 클래스를 주지 않아야 한다 (컴포넌트 기본 w-full 에 덮여 무효)', () => {
|
||||
const selects = [
|
||||
'pg_provider',
|
||||
'stock_deduction_timing',
|
||||
'mileage_deduction_timing',
|
||||
].map((field) =>
|
||||
findFirst(tpl, (n: any) =>
|
||||
n?.name === 'Select' && typeof n?.props?.value === 'string'
|
||||
&& n.props.value.includes(field),
|
||||
),
|
||||
);
|
||||
|
||||
for (const select of selects) {
|
||||
expect(select).not.toBeNull();
|
||||
expect(
|
||||
select.props.className ?? '',
|
||||
'Select 에 준 w-N 은 컴포넌트 기본 w-full 에 덮여 무효 — 죽은 클래스가 폭을 지정한 것처럼 오해를 준다',
|
||||
).not.toMatch(/\bw-\d+\b/);
|
||||
}
|
||||
});
|
||||
|
||||
it('고아 항목 삭제 버튼이 _orphaned 조건에서만 표시되어야 한다', () => {
|
||||
const deleteBtn = findFirst(tpl, (n: any) =>
|
||||
n?.name === 'Button' && typeof n?.if === 'string'
|
||||
@@ -825,6 +896,91 @@ describe('마일리지 차감 시점 결제수단별 컨트롤', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 고아 항목의 편집 컨트롤 차단 ───
|
||||
|
||||
describe('고아 결제수단 행의 편집 컨트롤 차단', () => {
|
||||
// 고아 항목 = 저장값은 남아 있으나 공급 확장이 더 이상 제공하지 않는 결제수단
|
||||
// (플러그인 삭제·비활성, 또는 그 확장이 자기 기능 토글을 끈 경우).
|
||||
// 이 행은 "지우세요" 만 제시해야 하므로 편집 컨트롤이 하나도 노출되면 안 된다.
|
||||
// 삭제 버튼(Button)은 반대로 고아일 때만 나오는 것이 정상이라 대상에서 제외한다.
|
||||
//
|
||||
// 회귀 배경: 리스트 파셜의 마일리지 차감 시점 열만 가드가 빠져 있었다. 다른 열이
|
||||
// 사라지면서 그 열이 오른쪽으로 밀려 최소 주문금액 자리에 표시됐고, 마일리지 기능을
|
||||
// 켠 상점에서는 이미 사라진 결제수단의 값을 바꿔 저장할 수 있었다. 같은 화면의
|
||||
// 카드 파셜은 편집 영역을 하나의 가드 컨테이너로 감싸 정상 차단하고 있었다.
|
||||
const EDIT_CONTROL_NAMES = ['Select', 'Input', 'Toggle', 'Checkbox', 'Textarea'];
|
||||
|
||||
const ORPHAN_GUARD = '!$method._orphaned';
|
||||
|
||||
/**
|
||||
* 편집 컨트롤마다 (조상 체인에 고아 가드가 있는가) 를 판정해 수집한다.
|
||||
*
|
||||
* 가드가 어느 깊이에 걸려 있든 통과시킨다 — 리스트는 열 컨테이너마다, 카드는
|
||||
* 편집 영역 전체를 감싼 컨테이너 한 곳에 걸려 있어 구조가 서로 다르기 때문이다.
|
||||
* 열 목록을 손으로 열거하지 않으므로 이후 추가되는 열도 자동으로 검사 대상이 된다.
|
||||
*/
|
||||
function collectEditControls(node: any, guarded = false, acc: any[] = []): any[] {
|
||||
if (!node || typeof node !== 'object') return acc;
|
||||
|
||||
const nowGuarded = guarded
|
||||
|| (typeof node.if === 'string' && node.if.includes(ORPHAN_GUARD));
|
||||
|
||||
if (EDIT_CONTROL_NAMES.includes(node.name)) {
|
||||
acc.push({ node, guarded: nowGuarded });
|
||||
}
|
||||
|
||||
for (const child of node.children ?? []) {
|
||||
collectEditControls(child, nowGuarded, acc);
|
||||
}
|
||||
if (node.itemTemplate) {
|
||||
collectEditControls(node.itemTemplate, nowGuarded, acc);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
/** 컨트롤을 사람이 읽을 수 있게 식별한다 (실패 메시지용). */
|
||||
function describeControl(entry: any): string {
|
||||
const value = entry.node.props?.value ?? entry.node.props?.checked ?? '';
|
||||
return `${entry.node.name}(${String(value).slice(0, 60) || 'no-value'})`;
|
||||
}
|
||||
|
||||
it.each([
|
||||
['리스트 파셜', paymentMethodsList],
|
||||
['카드 파셜', paymentMethodsCards],
|
||||
])('%s — 모든 편집 컨트롤이 고아 가드 아래에 있어야 한다', (_label, partial) => {
|
||||
const controls = collectEditControls(partial);
|
||||
|
||||
// 컨트롤을 하나도 못 찾았다면 탐색이 실패한 것이다 (부재 단언의 거짓 통과 차단)
|
||||
expect(controls.length).toBeGreaterThan(0);
|
||||
|
||||
const unguarded = controls.filter((c) => !c.guarded).map(describeControl);
|
||||
expect(
|
||||
unguarded,
|
||||
`고아 행에 편집 컨트롤이 노출된다: ${unguarded.join(', ')} — 조상 체인 어딘가에 if "${ORPHAN_GUARD}" 가 필요하다`,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('두 파셜이 같은 수의 편집 컨트롤을 가드해야 한다 (표시 방식 간 동작 일치)', () => {
|
||||
const listGuarded = collectEditControls(paymentMethodsList).filter((c) => c.guarded);
|
||||
const cardsGuarded = collectEditControls(paymentMethodsCards).filter((c) => c.guarded);
|
||||
|
||||
// 넓은 화면(리스트)과 좁은 화면(카드)은 같은 데이터를 그린다. 한쪽만 가드가 빠지면
|
||||
// 창 너비에 따라 편집 가능 여부가 달라진다.
|
||||
expect(listGuarded.length).toBeGreaterThan(0);
|
||||
expect(cardsGuarded.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('삭제 버튼은 고아일 때만 나오므로 편집 컨트롤 가드 대상이 아니다', () => {
|
||||
const deleteBtn = findFirst(paymentMethodsList, (n: any) =>
|
||||
n?.name === 'Button' && typeof n?.if === 'string'
|
||||
&& n.if.includes('$method._orphaned')
|
||||
&& !n.if.includes(ORPHAN_GUARD),
|
||||
);
|
||||
expect(deleteBtn).not.toBeNull();
|
||||
expect(EDIT_CONTROL_NAMES).not.toContain('Button');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 다국어 키 종합 검증 ───
|
||||
|
||||
describe('다국어 키 종합 검증', () => {
|
||||
|
||||
+6
-7
@@ -111,11 +111,12 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "PG 열 고정폭 — Select 는 커스텀 드롭다운이라 className 이 내부 Button 에 붙는데, 컴포넌트 기본 클래스의 w-full 이 CSS 출력 순서상 w-N 보다 뒤라 셀렉트에 직접 준 폭은 무효가 된다. 폭은 열 컨테이너가 책임진다 (미선택 '---' 상태에서 74px 로 붕괴하던 회귀 차단 + 선택값 길이에 따라 행마다 열 위치가 어긋나던 문제 해소).",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{!$method._orphaned}}",
|
||||
"props": {
|
||||
"className": "row-stack"
|
||||
"className": "row-stack w-36"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
@@ -133,7 +134,7 @@
|
||||
"if": "{{$method.pg_locked}}",
|
||||
"props": {
|
||||
"data-testid": "{{'pg-locked-badge-' + $method.id}}",
|
||||
"className": "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
|
||||
"className": "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 truncate"
|
||||
},
|
||||
"text": "{{$t('sirsoft-ecommerce.admin.settings.order_settings.payment_methods.pg_fixed') + ' · ' + (((_local.form?.available_pg_providers ?? []).find(p => p.id === $method.pg_provider))?.name ? $localized(((_local.form?.available_pg_providers ?? []).find(p => p.id === $method.pg_provider)).name) : ($method.pg_provider ?? ''))}}"
|
||||
},
|
||||
@@ -144,7 +145,6 @@
|
||||
"props": {
|
||||
"data-testid": "{{'pg-select-' + $method.id}}",
|
||||
"value": "{{$method.pg_provider ?? ''}}",
|
||||
"className": "w-36",
|
||||
"options": "{{[{value: '', label: '---'}].concat((_local.form?.available_pg_providers ?? []).filter(p => (p.supported_methods ?? []).includes($method.id)).map(p => ({value: p.id, label: $localized(p.name)})))}}",
|
||||
"disabled": "{{_computed.isReadOnly}}"
|
||||
},
|
||||
@@ -187,7 +187,7 @@
|
||||
"name": "Div",
|
||||
"if": "{{!$method._orphaned}}",
|
||||
"props": {
|
||||
"className": "row-stack"
|
||||
"className": "row-stack w-36"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
@@ -203,7 +203,6 @@
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"value": "{{$method.stock_deduction_timing ?? 'payment_complete'}}",
|
||||
"className": "w-36",
|
||||
"options": [
|
||||
{
|
||||
"value": "order_placed",
|
||||
@@ -237,8 +236,9 @@
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{!$method._orphaned}}",
|
||||
"props": {
|
||||
"className": "row-stack"
|
||||
"className": "row-stack w-36"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
@@ -254,7 +254,6 @@
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"value": "{{$method.mileage_deduction_timing ?? 'payment_complete'}}",
|
||||
"className": "w-36",
|
||||
"options": [
|
||||
{
|
||||
"value": "order_placed",
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ class EcommerceSettingsController extends PublicBaseController
|
||||
$this->logApiUsage('settings.checkout');
|
||||
|
||||
$shippingSettings = $this->settingsService->getSettings('shipping');
|
||||
$orderSettings = $this->settingsService->getSettings('order_settings');
|
||||
$orderSettings = $this->settingsService->getPublicPaymentSettings();
|
||||
|
||||
return ResponseHelper::moduleSuccess(
|
||||
'sirsoft-ecommerce',
|
||||
|
||||
+17
-4
@@ -132,11 +132,27 @@ trait HasMultiCurrencyPrices
|
||||
/**
|
||||
* 기본 통화 코드를 반환합니다.
|
||||
*
|
||||
* 기본 통화의 SSoT 는 설정의 `default_currency` 값이고 통화 항목의 `is_default` 는 그 파생입니다.
|
||||
* 표시 계층이 `is_default` 를 먼저 훑으면, 저장본에 없어 defaults 에서 보충된 통화가 옛 플래그를
|
||||
* 달고 들어왔을 때 환산 계층(CurrencyConversionService::getDefaultCurrency)과 서로 다른 통화를
|
||||
* 기본으로 잡습니다. 같은 화면의 금액이 통화별로 어긋나므로 `default_currency` 를 먼저 해석합니다.
|
||||
*
|
||||
* @return string 기본 통화 코드 (예: 'KRW')
|
||||
*/
|
||||
protected function getDefaultCurrencyCode(): string
|
||||
{
|
||||
$currencies = $this->getCurrencySettings();
|
||||
$settings = g7_module_settings('sirsoft-ecommerce', 'language_currency');
|
||||
$declared = $settings['default_currency'] ?? null;
|
||||
|
||||
// 선언된 기본 통화가 통화 목록에 실재할 때만 채택 (삭제된 통화를 가리키는 설정 방어)
|
||||
if ($declared !== null) {
|
||||
foreach ($currencies as $currency) {
|
||||
if (($currency['code'] ?? null) === $declared) {
|
||||
return $declared;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($currencies as $currency) {
|
||||
if ($currency['is_default'] ?? false) {
|
||||
@@ -144,10 +160,7 @@ trait HasMultiCurrencyPrices
|
||||
}
|
||||
}
|
||||
|
||||
// 설정이 없는 경우 모듈 설정에서 직접 조회
|
||||
$settings = g7_module_settings('sirsoft-ecommerce', 'language_currency');
|
||||
|
||||
return $settings['default_currency'] ?? 'KRW';
|
||||
return $declared ?? 'KRW';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -178,7 +178,7 @@ class CouponIssueRepository implements CouponIssueRepositoryInterface
|
||||
// coupon 관계를 직렬화하지 않지만, 이 목록은 훅
|
||||
// `sirsoft-ecommerce.user_coupon.filter_list_result` 로 확장에 그대로 전달된다.
|
||||
// 제3자 확장이 쿠폰 원본 정의(쿠폰명·할인율·사용조건)를 읽고 있을 수 있어 현행 유지
|
||||
// (PO 결정 2026-08-04). 번들 템플릿에는 아직 대응 마이페이지 화면이 없다.
|
||||
// (프로젝트 결정 2026-08-04). 번들 템플릿에는 아직 대응 마이페이지 화면이 없다.
|
||||
relations: ['coupon'],
|
||||
resultCap: PaginationLimits::resultCap('ecommerce.coupon_issues'),
|
||||
);
|
||||
|
||||
@@ -705,7 +705,7 @@ class CashReceiptService
|
||||
/**
|
||||
* 프로바이더 원응답에서 민감 키를 마스킹합니다.
|
||||
*
|
||||
* PO 결정: 전체 응답을 보관하되 알려진 민감 키를 재귀적으로 가린다.
|
||||
* 프로젝트 결정: 전체 응답을 보관하되 알려진 민감 키를 재귀적으로 가린다.
|
||||
*
|
||||
* @param mixed $response 원응답
|
||||
* @return array<string, mixed>|null 마스킹된 응답 (배열이 아니면 null)
|
||||
|
||||
@@ -126,6 +126,12 @@ class EcommerceSettingsService implements ModuleSettingsInterface
|
||||
);
|
||||
}
|
||||
|
||||
// 보충된 통화가 defaults 의 is_default 를 그대로 들여오면 기본 통화가 복수가 된다.
|
||||
// is_default 의 SSoT 는 default_currency 이므로 병합 직후 전 항목을 재동기화한다.
|
||||
if (isset($settings['language_currency'])) {
|
||||
$settings['language_currency'] = $this->syncCurrencyDefaults($settings['language_currency']);
|
||||
}
|
||||
|
||||
// 결제수단 병합 (기본 + 플러그인 필터 + 사용자 저장 설정)
|
||||
if (isset($settings['order_settings'])) {
|
||||
$settings['order_settings']['payment_methods'] = $this->getMergedPaymentMethods(
|
||||
@@ -188,14 +194,26 @@ class EcommerceSettingsService implements ModuleSettingsInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* 공개 결제 설정 조회 (bank_accounts에 은행명 매핑 포함)
|
||||
* 공개 결제 설정 조회 (고아 결제수단 제외 + bank_accounts에 은행명 매핑 포함)
|
||||
*
|
||||
* @return array 은행명이 포함된 결제 설정
|
||||
* 고아 결제수단(저장은 되어 있으나 현재 available 카탈로그에 없는 항목)은
|
||||
* 공급 확장이 사라졌거나 그 확장이 해당 수단 제공을 중단한 상태다. 저장값의
|
||||
* is_active 는 그대로 남아 있으므로 걸러내지 않으면 체크아웃이 선택 가능한
|
||||
* 결제수단으로 계속 노출한다(관리자 화면은 _orphaned 를 읽어 이미 차단).
|
||||
*
|
||||
* @return array 고아 항목이 제거되고 은행명이 포함된 결제 설정
|
||||
*/
|
||||
public function getPublicPaymentSettings(): array
|
||||
{
|
||||
$orderSettings = $this->getSettings('order_settings');
|
||||
|
||||
if (isset($orderSettings['payment_methods']) && is_array($orderSettings['payment_methods'])) {
|
||||
$orderSettings['payment_methods'] = array_values(array_filter(
|
||||
$orderSettings['payment_methods'],
|
||||
fn ($method) => ! ($method['_orphaned'] ?? false)
|
||||
));
|
||||
}
|
||||
|
||||
if (isset($orderSettings['bank_accounts'], $orderSettings['banks'])) {
|
||||
$banks = collect($orderSettings['banks']);
|
||||
$orderSettings['bank_accounts'] = array_map(function ($account) use ($banks) {
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Modules\Sirsoft\Ecommerce\Support;
|
||||
* 를 이 계약이 도입된 `>=1.1.0` 이상으로 선언해야 한다.
|
||||
*
|
||||
* 시그니처를 바꾸거나 제거할 때는 위 소비 확장을 전수 확인하고 각 확장의 최소 버전
|
||||
* 제약을 함께 올린다 (CLAUDE.md "버전 동기화 의무" ②).
|
||||
* 제약(`dependencies.modules.sirsoft-ecommerce`)을 함께 올린다.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Controllers\Public;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
|
||||
|
||||
/**
|
||||
* 공개 결제 설정 API 의 고아 결제수단 차단 테스트
|
||||
*
|
||||
* 회귀 배경: 토스페이먼츠 플러그인의 주문서형 결제를 끄면 toss_* 결제수단이 available
|
||||
* 카탈로그에서 빠지지만 저장값의 is_active 는 true 로 남는다. 관리자 화면은 고아 표시로
|
||||
* 차단하는데 체크아웃은 is_active 만 보므로, 공개 API 가 걸러 주지 않으면 비활성화한
|
||||
* 결제수단이 주문서에서 그대로 선택 가능했다. 플러그인 삭제/비활성 시에도 동일하다.
|
||||
*/
|
||||
class PaymentSettingsOrphanedMethodTest extends ModuleTestCase
|
||||
{
|
||||
private const PAYMENT_ENDPOINT = '/api/modules/sirsoft-ecommerce/settings/payment';
|
||||
|
||||
private const CHECKOUT_ENDPOINT = '/api/modules/sirsoft-ecommerce/settings/checkout';
|
||||
|
||||
private const ORPHANED_ID = 'toss_kakaopay';
|
||||
|
||||
private string $storagePath;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->storagePath = storage_path('framework/testing/modules/sirsoft-ecommerce/settings');
|
||||
|
||||
$this->saveOrderSettings([
|
||||
'payment_methods' => [
|
||||
['id' => 'card', 'sort_order' => 1, 'is_active' => true, 'min_order_amount' => 0, 'stock_deduction_timing' => 'payment_complete'],
|
||||
['id' => 'dbank', 'sort_order' => 3, 'is_active' => true, 'min_order_amount' => 0, 'stock_deduction_timing' => 'order_placed'],
|
||||
[
|
||||
// 공급 플러그인이 더 이상 제공하지 않는 결제수단 (필터 미등록 → available 부재)
|
||||
'id' => self::ORPHANED_ID,
|
||||
'sort_order' => 121,
|
||||
'is_active' => true,
|
||||
'min_order_amount' => 0,
|
||||
'stock_deduction_timing' => 'payment_complete',
|
||||
'_cached_name' => ['ko' => '카카오페이 (토스페이먼츠)', 'en' => 'KakaoPay (TossPayments)'],
|
||||
'_cached_icon' => 'wallet',
|
||||
'_cached_source' => 'plugin:sirsoft-tosspayments',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if (File::isDirectory($this->storagePath)) {
|
||||
File::cleanDirectory($this->storagePath);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_payment_endpoint_excludes_orphaned_method(): void
|
||||
{
|
||||
$response = $this->getJson(self::PAYMENT_ENDPOINT);
|
||||
|
||||
$response->assertOk();
|
||||
$ids = array_column($response->json('data.order_settings.payment_methods'), 'id');
|
||||
|
||||
$this->assertNotContains(self::ORPHANED_ID, $ids);
|
||||
$this->assertContains('card', $ids);
|
||||
$this->assertContains('dbank', $ids);
|
||||
}
|
||||
|
||||
public function test_checkout_endpoint_excludes_orphaned_method(): void
|
||||
{
|
||||
$response = $this->getJson(self::CHECKOUT_ENDPOINT);
|
||||
|
||||
$response->assertOk();
|
||||
$ids = array_column($response->json('data.order_settings.payment_methods'), 'id');
|
||||
|
||||
$this->assertNotContains(self::ORPHANED_ID, $ids);
|
||||
$this->assertContains('card', $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* order_settings.json 을 테스트 격리 경로에 기록합니다.
|
||||
*
|
||||
* @param array $settings 저장할 주문설정 배열
|
||||
*/
|
||||
private function saveOrderSettings(array $settings): void
|
||||
{
|
||||
File::ensureDirectoryExists($this->storagePath);
|
||||
File::put(
|
||||
$this->storagePath.'/order_settings.json',
|
||||
json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* 이커머스 모듈 Playwright E2E 설정.
|
||||
*
|
||||
* 코어 `playwright.config.ts` 와 동일한 base URL 해석 우선순위를 따른다 — 모듈도 활성 호스트가
|
||||
* 가변(개발자/CI/PO 환경별로 다른 도메인)이므로 하드코딩 회피.
|
||||
* 가변(개발자/CI/운영 환경별로 다른 도메인)이므로 하드코딩 회피.
|
||||
*
|
||||
* Base URL 해석:
|
||||
* 1. PLAYWRIGHT_BASE_URL 환경변수 (CI/명시적 오버라이드)
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 주문설정 결제수단 — PG 플러그인 브랜드 마크 주입 회귀.
|
||||
*
|
||||
* 배경: 관리자 레이아웃은 아이콘 열을 `Icon name={{$method._cached_icon}}` 하나로만 그리고
|
||||
* `_cached_brand_mark` 를 읽는 노드가 없다. 각 PG 플러그인이 자기 결제수단 행의 아이콘을
|
||||
* 브랜드 배지로 치환하는 관리자 전용 injector 를 직접 싣는 구조다.
|
||||
*
|
||||
* 이 spec 이 고정하는 두 가지:
|
||||
* 1. 설치된 PG 플러그인의 간편결제 행에 브랜드 배지가 주입된다 (플러그인별 data 속성으로 판정).
|
||||
* 2. **다른 탭에 갔다 돌아와도** 배지가 재주입된다. 탭 전환은 pushState 가 아니라
|
||||
* replaceState 로 URL 만 바꾸므로, pushState/popstate 만 후킹하던 구현은 복귀 후
|
||||
* 배지가 사라진 채로 남았다(행은 정상 렌더되어 화면상 아이콘만 회색으로 되돌아감).
|
||||
*
|
||||
* 측정 규율:
|
||||
* - 결제수단 ID·PG명을 하드코딩하지 않는다. 설치 구성은 환경마다 다르므로 브랜드 배지
|
||||
* data 속성을 **런타임 카운트**로 판정하고, 배지가 0인 환경(간편결제 플러그인 미설치)은
|
||||
* skip 이 아니라 "행이 렌더됐는지" 를 먼저 확인해 거짓 통과를 만들지 않는다.
|
||||
*
|
||||
* 마킹은 쉼표로 축을 나눈다 — 마커 파서가 `,` 로만 분리하므로, 저장소에 흔한 `×` 표기는
|
||||
* 첫 축만 잡히고 나머지가 값 문자열에 묻혀 cross product 커버로 집계되지 않는다.
|
||||
*
|
||||
* @scenario extension_payment_method, method_kind=extension, capability_declared=declared, capability=brand_mark
|
||||
* @effects admin_shows_brand_mark, admin_rebinds_brand_mark_after_tab_return
|
||||
*/
|
||||
import { test, expect, authenticatePage } from '../../fixtures/ecommerce-auth';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
const ORDER_SETTINGS_URL = '/admin/ecommerce/settings?tab=order_settings';
|
||||
|
||||
/**
|
||||
* 렌더된 결제수단 행 수.
|
||||
*
|
||||
* 행 판정에 `pg-locked-badge-*` 하나만 쓰지 않는다 — 그 배지는 `pg_locked === true` 인 수단에만
|
||||
* 렌더되므로, PG 고정 수단이 하나도 없는 구성에서는 화면이 정상 렌더돼도 0 이 되어 준비 게이트가
|
||||
* 영원히 열리지 않는다(설치 구성을 하드코딩하지 않는다는 이 spec 의 규율과 어긋난다).
|
||||
*
|
||||
* PG 열은 비고아 행마다 정확히 하나의 분기를 그린다 — pg_locked / PG 셀렉트 / PG 미설치 /
|
||||
* PG 불필요. 네 접두사를 합치면 설치 구성과 무관하게 비고아 행 수와 일치한다.
|
||||
*/
|
||||
const PG_CELL_TESTID_PREFIXES = [
|
||||
'pg-locked-badge-',
|
||||
'pg-select-',
|
||||
'pg-not-installed-',
|
||||
'pg-not-required-',
|
||||
] as const;
|
||||
|
||||
async function rowCount(page: Page): Promise<number> {
|
||||
return page.evaluate(
|
||||
(prefixes) =>
|
||||
prefixes.reduce(
|
||||
(sum, prefix) => sum + document.querySelectorAll(`[data-testid^="${prefix}"]`).length,
|
||||
0,
|
||||
),
|
||||
PG_CELL_TESTID_PREFIXES as unknown as string[],
|
||||
);
|
||||
}
|
||||
|
||||
/** 플러그인별 브랜드 배지 카운트 (data 속성 접미사로 집계 — 플러그인명 하드코딩 회피). */
|
||||
async function badgeCountsByPlugin(page: Page): Promise<Record<string, number>> {
|
||||
return page.evaluate(() => {
|
||||
const out: Record<string, number> = {};
|
||||
for (const el of document.querySelectorAll<HTMLElement>('span[aria-hidden="true"]')) {
|
||||
for (const name of Object.keys(el.dataset)) {
|
||||
if (!/AdminPaymentBrandMark$/.test(name)) continue;
|
||||
out[name] = (out[name] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
/** 설정 탭을 라벨 텍스트로 클릭한다 (탭은 role/accessible-name 이 라벨과 일치하지 않는다). */
|
||||
async function clickTab(page: Page, label: string): Promise<void> {
|
||||
const clicked = await page.evaluate((text) => {
|
||||
const el = [...document.querySelectorAll('button, a')].find((e) => e.textContent?.trim() === text);
|
||||
if (!el) return false;
|
||||
(el as HTMLElement).click();
|
||||
return true;
|
||||
}, label);
|
||||
if (!clicked) throw new Error(`탭을 찾지 못했다: ${label}`);
|
||||
}
|
||||
|
||||
async function gotoOrderSettings(page: Page, token: string): Promise<void> {
|
||||
await authenticatePage(page, token);
|
||||
await page.setViewportSize({ width: 1440, height: 1000 });
|
||||
await page.goto(ORDER_SETTINGS_URL);
|
||||
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
|
||||
await expect.poll(async () => await rowCount(page), { timeout: 30_000 }).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
test.describe('@sirsoft-ecommerce 주문설정 결제수단 — 브랜드 마크', () => {
|
||||
test('PG 플러그인의 간편결제 행에 브랜드 배지가 주입된다', async ({ page, settingsToken }) => {
|
||||
await gotoOrderSettings(page, settingsToken);
|
||||
|
||||
await expect
|
||||
.poll(async () => Object.values(await badgeCountsByPlugin(page)).reduce((a, b) => a + b, 0), { timeout: 20_000 })
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
const byPlugin = await badgeCountsByPlugin(page);
|
||||
// 간편결제를 제공하는 PG 플러그인이 하나라도 설치돼 있으면 그 플러그인 배지가 잡혀야 한다.
|
||||
expect(Object.keys(byPlugin).length, `브랜드 배지를 주입한 플러그인이 없다: ${JSON.stringify(byPlugin)}`)
|
||||
.toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('다른 탭에 갔다 돌아와도 브랜드 배지가 재주입된다 (replaceState 회귀)', async ({ page, settingsToken }) => {
|
||||
await gotoOrderSettings(page, settingsToken);
|
||||
|
||||
await expect
|
||||
.poll(async () => Object.values(await badgeCountsByPlugin(page)).reduce((a, b) => a + b, 0), { timeout: 20_000 })
|
||||
.toBeGreaterThan(0);
|
||||
const before = await badgeCountsByPlugin(page);
|
||||
|
||||
// 탭 전환은 replaceState 로 URL 만 바꾼다 — 화면 전환을 실제 클릭으로 재현한다.
|
||||
// 탭은 접근성 이름이 라벨과 정확히 일치하지 않으므로(내부 텍스트 노드 구성) 텍스트로 찾는다.
|
||||
await clickTab(page, '배송설정');
|
||||
await expect.poll(async () => await rowCount(page), { timeout: 20_000 }).toBe(0);
|
||||
|
||||
await clickTab(page, '주문설정');
|
||||
await expect.poll(async () => await rowCount(page), { timeout: 20_000 }).toBeGreaterThan(0);
|
||||
|
||||
// 복귀 후 배지가 수정 전처럼 0 으로 남지 않아야 한다.
|
||||
await expect
|
||||
.poll(async () => Object.values(await badgeCountsByPlugin(page)).reduce((a, b) => a + b, 0), { timeout: 20_000 })
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
const after = await badgeCountsByPlugin(page);
|
||||
expect(after, `탭 복귀 후 배지가 복원되지 않았다 (before=${JSON.stringify(before)} after=${JSON.stringify(after)})`)
|
||||
.toEqual(before);
|
||||
});
|
||||
});
|
||||
+75
@@ -173,6 +173,81 @@ test.describe('@sirsoft-ecommerce 주문설정 결제수단 — PG 고정 표시
|
||||
}
|
||||
});
|
||||
|
||||
// @scenario extension_payment_method:pg_select_for_unlocked
|
||||
// @effects admin_shows_pg_select_for_unlocked
|
||||
test('PG 셀렉트는 미선택 상태에서도 형제 필드 열과 같은 폭으로 렌더된다', async ({
|
||||
page,
|
||||
settingsToken,
|
||||
}) => {
|
||||
// 배경: Select 는 커스텀 드롭다운이라 레이아웃이 준 className 이 내부 button 에 붙는데,
|
||||
// 컴포넌트 기본 클래스의 w-full 이 CSS 출력 순서상 w-N 보다 뒤라 폭 토큰이 무효가 됐다.
|
||||
// 그 결과 폭이 내용 폭을 따라가 미선택('---') 상태에서 74px 로 붕괴했고, 선택값 길이에
|
||||
// 따라 행마다 열 위치가 어긋났다. 폭은 열 컨테이너가 책임진다.
|
||||
//
|
||||
// 측정 규율: 절대 픽셀값을 기대치로 박지 않는다 (폰트·테마·PG 구성에 따라 달라진다).
|
||||
// 같은 행의 형제 필드 열과 **상대 비교**하고, 행 간 열 폭이 일치하는지를 본다.
|
||||
await gotoOrderSettings(page, settingsToken);
|
||||
|
||||
// 고정폭이 유지되는 넓은 뷰포트에서 측정한다. 좁은 폭에서는 세 필드 열이 함께
|
||||
// 비례 축소되므로(행 넘침 차단) 폭의 절대 일치가 성립하지 않는다.
|
||||
await page.setViewportSize({ width: 1600, height: 1000 });
|
||||
|
||||
// 카탈로그 시드(gotoOrderSettings)와 DOM 렌더는 별개다 — 렌더를 기다리지 않으면
|
||||
// 측정 결과가 비어 skip 되고, 그 skip 이 거짓 통과가 된다.
|
||||
await expect(
|
||||
page.locator('[data-testid^="pg-select-"]').first(),
|
||||
'PG 선택 셀렉트가 렌더되지 않아 폭을 측정할 수 없다',
|
||||
).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
const measured = await page.evaluate(() => {
|
||||
const selects = Array.from(document.querySelectorAll('[data-testid^="pg-select-"]'));
|
||||
const rows = selects
|
||||
.map((el) => (el.parentElement as HTMLElement | null)?.parentElement ?? null)
|
||||
.filter((r): r is HTMLElement => r !== null);
|
||||
|
||||
return rows.map((row) => {
|
||||
const columns = Array.from(row.children).filter((c) =>
|
||||
c.className.includes('row-stack'),
|
||||
) as HTMLElement[];
|
||||
const pgSelect = row.querySelector('[data-testid^="pg-select-"]');
|
||||
return {
|
||||
id: pgSelect?.getAttribute('data-testid') ?? '',
|
||||
label: (pgSelect?.querySelector('button') as HTMLElement | null)?.innerText.trim() ?? '',
|
||||
columnWidths: columns.map((c) => Math.round(c.getBoundingClientRect().width)),
|
||||
columnLefts: columns.map((c) => Math.round(c.getBoundingClientRect().left)),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// 여기까지 왔으면 셀렉트는 렌더된 상태다. 측정이 비었다면 DOM 탐색이 깨진 것이므로
|
||||
// skip 이 아니라 실패로 드러내야 한다 (skip 은 거짓 통과가 된다).
|
||||
expect(measured.length, 'PG 셀렉트를 담은 행을 찾지 못했다 — 측정 경로가 깨졌다').toBeGreaterThan(0);
|
||||
|
||||
for (const row of measured) {
|
||||
const [pgWidth, ...siblingWidths] = row.columnWidths;
|
||||
expect(
|
||||
siblingWidths.length,
|
||||
`${row.id} 행에서 비교할 형제 필드 열을 찾지 못했다 — 측정 경로가 깨졌다`,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// 셀렉트 열이 형제 필드 열보다 좁으면 내용 폭으로 붕괴한 것이다.
|
||||
// (미선택 '---' 은 글자 폭이 가장 작아 회귀가 여기서 가장 크게 드러난다.)
|
||||
const widest = Math.max(...siblingWidths);
|
||||
expect(
|
||||
pgWidth,
|
||||
`${row.id}("${row.label}") 의 PG 열(${pgWidth}px)이 형제 필드 열(${widest}px)보다 좁다 — 셀렉트 폭이 내용 폭을 따라간 회귀`,
|
||||
).toBeGreaterThanOrEqual(widest);
|
||||
}
|
||||
|
||||
// 행마다 PG 열 폭이 다르면 선택값 길이가 레이아웃을 좌우한 것이다
|
||||
// (미선택 행만 좁아지거나, 긴 PG 명을 고른 행만 넓어진다).
|
||||
const pgWidths = new Set(measured.map((r) => r.columnWidths[0]));
|
||||
expect(
|
||||
pgWidths.size,
|
||||
`행마다 PG 열 폭이 다르다 (${[...pgWidths].join(' / ')}px) — 선택값 길이가 열 폭을 좌우하는 회귀`,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
// @scenario extension_payment_method:no_pg_required
|
||||
// @effects admin_shows_pg_select_for_unlocked
|
||||
test('PG 불필요 수단(무통장·포인트 등)은 셀렉트도 배지도 없이 "PG 불필요"로 남는다', async ({
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@
|
||||
* 부작용: ②③ 은 실제 주문을 생성한다(결제 대기 상태). 무통장입금은 외부 PG 를 거치지 않으므로
|
||||
* 샌드박스 의존이 없고, 생성된 주문은 설정된 기한이 지나면 미입금 자동취소 대상이 된다.
|
||||
*
|
||||
* @see .claude/docs/frontend/troubleshooting-backend.md 사례 21
|
||||
* 회귀 배경: 입금기한 일수 설정이 문자열로 영속되면 기한 산정이 Carbon 의 strict 타입 경계에서
|
||||
* TypeError 를 던져 해당 주문 요청이 전부 500 이 되던 결함이 있었다.
|
||||
*/
|
||||
import { test, expect, authenticatePage } from '../../fixtures/ecommerce-auth';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
@@ -12,6 +12,9 @@ use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
|
||||
|
||||
/**
|
||||
* OrderPayment 모델 테스트
|
||||
*
|
||||
* @scenario extension_payment_method method_kind=extension × capability_declared=declared × capability=refund_method
|
||||
* @effects extension_id_persisted_as_is, builtin_capability_unchanged, refund_method_is_pg
|
||||
*/
|
||||
class OrderPaymentTest extends ModuleTestCase
|
||||
{
|
||||
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Modules\Sirsoft\Ecommerce\Http\Resources\Traits\HasMultiCurrencyPrices;
|
||||
use Modules\Sirsoft\Ecommerce\Services\CurrencyConversionService;
|
||||
use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService;
|
||||
use Modules\Sirsoft\Ecommerce\Support\CurrencySettingsCache;
|
||||
use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
|
||||
|
||||
/**
|
||||
* 기본 통화 플래그 단일성 테스트 (#543 — PG 청구 1/100 회귀)
|
||||
*
|
||||
* 저장본에 없는 통화를 defaults 에서 보충할 때(mergeCurrenciesByCode) 보충 항목의
|
||||
* is_default 를 그대로 들여오면 기본 통화가 2개가 된다. 그 상태에서
|
||||
* - buildCurrencySnapshot 이 `is_default ? 1.0 : exchange_rate` 로 환율을 1.0 으로 박제하고
|
||||
* - 표시 계층(HasMultiCurrencyPrices)이 앞선 is_default 통화를 기본으로 오인해
|
||||
* 화면 총액과 PG 청구액이 어긋난다 (base JPY 23,000 → 토스 230 KRW).
|
||||
*
|
||||
* is_default 의 SSoT 는 저장본의 default_currency 값이며, 보충된 통화의 is_default 는 기각한다.
|
||||
*/
|
||||
class EcommerceSettingsCurrencyDefaultFlagTest extends ModuleTestCase
|
||||
{
|
||||
private EcommerceSettingsService $service;
|
||||
|
||||
private string $storagePath;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->storagePath = storage_path('framework/testing/modules/sirsoft-ecommerce/settings');
|
||||
|
||||
if (File::isDirectory($this->storagePath)) {
|
||||
File::cleanDirectory($this->storagePath);
|
||||
}
|
||||
|
||||
$this->service = new EcommerceSettingsService;
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if (File::isDirectory($this->storagePath)) {
|
||||
File::cleanDirectory($this->storagePath);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* language_currency.json 을 직접 생성하여 저장된 통화 설정을 시뮬레이션합니다.
|
||||
*/
|
||||
private function saveLanguageCurrency(array $settings): void
|
||||
{
|
||||
File::ensureDirectoryExists($this->storagePath);
|
||||
File::put(
|
||||
$this->storagePath.'/language_currency.json',
|
||||
json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 병합 결과에서 is_default 가 true 인 통화 코드 목록을 반환합니다.
|
||||
*/
|
||||
private function defaultCodes(): array
|
||||
{
|
||||
$this->service->clearCache();
|
||||
$lc = $this->service->getSettings('language_currency');
|
||||
|
||||
return collect($lc['currencies'])
|
||||
->filter(fn ($c) => ($c['is_default'] ?? false) === true)
|
||||
->pluck('code')
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 병합 결과를 런타임과 동일하게 config 로 발행합니다.
|
||||
*
|
||||
* 실제 요청에서는 CoreServiceProvider 가 부팅 시 getAllSettings() 결과를
|
||||
* `g7_settings.modules.{id}` 에 넣고, 표시/환산 계층은 그 config 만 읽습니다.
|
||||
* 테스트는 파일을 직접 쓰므로 같은 발행 단계를 재현해야 합니다.
|
||||
*/
|
||||
private function publishSettingsToConfig(): void
|
||||
{
|
||||
$this->service->clearCache();
|
||||
CurrencySettingsCache::clear();
|
||||
|
||||
Config::set(
|
||||
'g7_settings.modules.sirsoft-ecommerce.language_currency',
|
||||
$this->service->getSettings('language_currency')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 운영 재현 저장본: 기본 통화를 JPY 로 바꾸고 JPY/USD 두 건만 저장한 상태.
|
||||
*/
|
||||
private function saveJpyBaseSubset(): void
|
||||
{
|
||||
$this->saveLanguageCurrency([
|
||||
'default_currency' => 'JPY',
|
||||
'currencies' => [
|
||||
['code' => 'JPY', 'name' => ['ja' => '円'], 'is_default' => true, 'decimal_places' => 0, 'base_unit' => 100, 'exchange_rate' => null],
|
||||
['code' => 'USD', 'name' => ['en' => 'Dollar'], 'is_default' => false, 'decimal_places' => 2, 'base_unit' => 1, 'exchange_rate' => 0.85],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 보충 통화의 is_default 기각
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
public function test_backfilled_currency_default_flag_is_rejected(): void
|
||||
{
|
||||
// defaults 의 KRW 는 is_default: true 이며 저장본에 없어 보충 대상이 된다.
|
||||
$this->saveJpyBaseSubset();
|
||||
|
||||
$this->assertSame(['JPY'], $this->defaultCodes(), '보충된 통화의 is_default 가 기각되지 않아 기본 통화가 복수입니다.');
|
||||
}
|
||||
|
||||
public function test_default_currency_is_unique_across_all_saved_shapes(): void
|
||||
{
|
||||
$this->saveJpyBaseSubset();
|
||||
|
||||
$this->service->clearCache();
|
||||
$lc = $this->service->getSettings('language_currency');
|
||||
|
||||
$flags = collect($lc['currencies'])->map(fn ($c) => (bool) ($c['is_default'] ?? false));
|
||||
|
||||
$this->assertSame(1, $flags->filter()->count(), '기본 통화 플래그는 정확히 1개여야 합니다.');
|
||||
$this->assertSame('JPY', $lc['default_currency']);
|
||||
}
|
||||
|
||||
public function test_default_currency_absent_from_saved_list_is_promoted_when_backfilled(): void
|
||||
{
|
||||
// 기본 통화(CNY)가 저장본 currencies 에는 없어 보충 대상이 되는 경우 —
|
||||
// 보충 항목의 플래그를 기각만 하면 기본 통화가 0개가 된다.
|
||||
$this->saveLanguageCurrency([
|
||||
'default_currency' => 'CNY',
|
||||
'currencies' => [
|
||||
['code' => 'USD', 'name' => ['en' => 'Dollar'], 'is_default' => false, 'decimal_places' => 2, 'base_unit' => 1, 'exchange_rate' => 0.85],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSame(['CNY'], $this->defaultCodes(), '보충된 기본 통화가 승격되지 않아 기본 통화가 사라졌습니다.');
|
||||
}
|
||||
|
||||
public function test_nothing_saved_keeps_defaults_base_currency(): void
|
||||
{
|
||||
// 저장본 없음(신규 설치) → defaults 의 default_currency(KRW) 유지
|
||||
$this->assertSame(['KRW'], $this->defaultCodes());
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 기본 통화 해석 SSoT (표시 계층 ↔ 환산 계층 일치)
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
public function test_display_layer_resolves_same_base_currency_as_conversion_layer(): void
|
||||
{
|
||||
$this->saveJpyBaseSubset();
|
||||
$this->publishSettingsToConfig();
|
||||
|
||||
$resource = new class
|
||||
{
|
||||
use HasMultiCurrencyPrices {
|
||||
getDefaultCurrencyCode as public exposedDefaultCurrencyCode;
|
||||
getDefaultBaseUnit as public exposedDefaultBaseUnit;
|
||||
}
|
||||
};
|
||||
|
||||
$resource::clearCurrencySettingsCache();
|
||||
$conversion = new CurrencyConversionService;
|
||||
|
||||
$this->assertSame('JPY', $resource->exposedDefaultCurrencyCode(), '표시 계층이 기본 통화를 오인했습니다.');
|
||||
$this->assertSame('JPY', $conversion->getDefaultCurrency());
|
||||
$this->assertSame(100, $resource->exposedDefaultBaseUnit(), '표시 계층의 환산 분모가 기본 통화 base_unit 과 다릅니다.');
|
||||
$this->assertSame(100, $conversion->getDefaultBaseUnit());
|
||||
}
|
||||
|
||||
public function test_unconfigured_exchange_rate_currency_is_not_treated_as_base(): void
|
||||
{
|
||||
// KRW 는 환율 미입력(null) 상태로 보충된다. 기본 통화로 오인되면 환율이 1.0 으로
|
||||
// 박제되어 PG 청구가 base 금액의 1/base_unit 으로 나간다(#543).
|
||||
$this->saveJpyBaseSubset();
|
||||
$this->service->clearCache();
|
||||
|
||||
$lc = $this->service->getSettings('language_currency');
|
||||
$krw = collect($lc['currencies'])->firstWhere('code', 'KRW');
|
||||
|
||||
$this->assertFalse((bool) ($krw['is_default'] ?? false));
|
||||
$this->assertNull($krw['exchange_rate'], '환율 미입력 통화는 null 로 남아 미지원 통화 차단에 걸려야 합니다.');
|
||||
}
|
||||
}
|
||||
+51
@@ -473,6 +473,57 @@ class EcommerceSettingsOrderSettingsTest extends ModuleTestCase
|
||||
$this->assertEquals('tosspayments', $lastMethod['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공개 결제 설정에서는 고아 결제수단이 제거되어야 한다.
|
||||
*
|
||||
* 회귀 배경: 토스페이먼츠 플러그인의 주문서형 결제를 끄면 toss_* 결제수단이
|
||||
* available 카탈로그에서 빠지지만 저장값의 is_active 는 true 로 남는다.
|
||||
* 관리자 화면은 _orphaned 를 읽어 차단하는데 체크아웃은 is_active 만 보므로,
|
||||
* 서버가 걸러주지 않으면 비활성화된 결제수단이 그대로 선택 가능했다.
|
||||
*/
|
||||
public function test_public_payment_settings_exclude_orphaned_methods(): void
|
||||
{
|
||||
$savedSettings = [
|
||||
'payment_methods' => [
|
||||
['id' => 'card', 'sort_order' => 1, 'is_active' => true, 'min_order_amount' => 0, 'stock_deduction_timing' => 'payment_complete'],
|
||||
['id' => 'dbank', 'sort_order' => 3, 'is_active' => true, 'min_order_amount' => 0, 'stock_deduction_timing' => 'order_placed'],
|
||||
[
|
||||
'id' => 'toss_kakaopay',
|
||||
'sort_order' => 121,
|
||||
'is_active' => true,
|
||||
'min_order_amount' => 0,
|
||||
'stock_deduction_timing' => 'payment_complete',
|
||||
'_cached_name' => ['ko' => '카카오페이 (토스페이먼츠)', 'en' => 'KakaoPay (TossPayments)'],
|
||||
'_cached_icon' => 'wallet',
|
||||
'_cached_source' => 'plugin:sirsoft-tosspayments',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->saveOrderSettings($savedSettings);
|
||||
$this->service->clearCache();
|
||||
|
||||
// 플러그인 필터 미등록 → toss_kakaopay 는 available 에 없다(주문서형 결제 OFF 와 동일 상태)
|
||||
$adminMethods = $this->service->getSettings('order_settings')['payment_methods'];
|
||||
$adminToss = collect($adminMethods)->firstWhere('id', 'toss_kakaopay');
|
||||
$this->assertNotNull($adminToss, '관리자 응답에는 고아 항목이 남아 있어야 한다(삭제 UI 필요)');
|
||||
$this->assertTrue($adminToss['_orphaned'] ?? false);
|
||||
|
||||
$publicMethods = $this->service->getPublicPaymentSettings()['payment_methods'];
|
||||
$publicIds = array_column($publicMethods, 'id');
|
||||
|
||||
$this->assertNotContains('toss_kakaopay', $publicIds);
|
||||
$this->assertContains('card', $publicIds);
|
||||
$this->assertContains('dbank', $publicIds);
|
||||
|
||||
// 리스트 인덱스가 재정렬되어야 한다 (JSON 직렬화 시 객체가 되지 않도록)
|
||||
$this->assertSame(range(0, count($publicMethods) - 1), array_keys($publicMethods));
|
||||
|
||||
foreach ($publicMethods as $method) {
|
||||
$this->assertArrayNotHasKey('_orphaned', $method);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// _cached_* 메타데이터 스냅샷
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ notes: |
|
||||
(`OrderCalculationService`)가 그 관계를 직접 읽고, 미로드 시 항목마다 재조회하므로 좁히면
|
||||
페이로드 대신 쿼리가 늘어난다. 게다가 계산기가 `is_active` 를 거르지 않아 로드 경로에 따라
|
||||
배송비가 달라질 수 있다 — 계산 경로를 함께 손대야 안전한 축이라 본 이슈에서 다루지 않는다
|
||||
(PO 결정, 2026-08-05).
|
||||
(프로젝트 결정, 2026-08-05).
|
||||
|
||||
E2E 미해당: 응답 형태가 불변이라 브라우저 경로에 새 분기가 없다. 소비 사실 고정은
|
||||
레이아웃 원문 검사(`cartWishlistImageConsumers.test.ts`)가 담당한다.
|
||||
+3
-3
@@ -11,7 +11,7 @@ description: |
|
||||
리터럴 객체다. 슬롯에 주입된 모듈 폼이 _local 에 값을 써도 그 body 에 실릴 방법이 없다 —
|
||||
결제 버튼에 id 가 없어 overlay 로 타겟할 수 없고, inject_props 는 컴포넌트 props 전용이라
|
||||
actions[].params.body 에 도달하지 못한다. 그대로 두면 구매자가 신청해도 주문에 아무 기록이 남지 않는다.
|
||||
→ PO 결정: body 를 통짜 표현식으로 바꾸고 확장 병합 칸(...(_local.checkoutExtraPayload ?? {})) 한 개를 연다.
|
||||
→ 프로젝트 결정: body 를 통짜 표현식으로 바꾸고 확장 병합 칸(...(_local.checkoutExtraPayload ?? {})) 한 개를 연다.
|
||||
템플릿은 현금영수증을 알지 않고, 모듈은 그 칸에만 쓴다. 확장 미주입 시 payload 는 전환 전과 동일하다.
|
||||
|
||||
발견한 사전 결함(본 세션에서 수정):
|
||||
@@ -29,7 +29,7 @@ description: |
|
||||
· 발급 버튼과 재발급 버튼의 노출 조건이 배타적이지 않아 FAILED 상태에서 둘 다 떴다.
|
||||
· 이력 표가 원시값을 노출했다 — 취소 행에 "발급 완료", 실패 행 일시 "-"(issued_at null), issue/FAILED.
|
||||
|
||||
환불계좌는 PG·현금영수증 프로바이더와 무관한 순수 주문 정보이므로 템플릿이 직접 소유한다(PO 결정).
|
||||
환불계좌는 PG·현금영수증 프로바이더와 무관한 순수 주문 정보이므로 템플릿이 직접 소유한다(프로젝트 결정).
|
||||
취소 시점 갱신은 레이아웃이 아니라 cancelOrderHandlers.ts 가 수행한다 — 취소 POST body 를
|
||||
그 핸들러가 조립하기 때문이다.
|
||||
|
||||
@@ -215,7 +215,7 @@ effects:
|
||||
- portable_override_applies_below_1024_on_render # 실렌더: 세로 스택 전환 (F5)
|
||||
- portable_override_absent_at_1024_on_render # 실렌더: 1024px 부터 base 복귀
|
||||
|
||||
# 데이터 위생 (감사 지적 C8, PO 결정 2026-07-10)
|
||||
# 데이터 위생 (감사 지적 C8, 프로젝트 결정 2026-07-10)
|
||||
- cash_receipt_request_not_persisted_for_non_dbank # 발급 불가 주문에 암호문 미보관
|
||||
|
||||
# 자진발급 설정 안내 (감사 지적 W-5)
|
||||
|
||||
@@ -16,6 +16,7 @@ test_files:
|
||||
- modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/OrderPaymentTest.php
|
||||
- modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsOrder.test.tsx
|
||||
- modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-pg-locked.spec.ts
|
||||
- modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-brand-mark.spec.ts
|
||||
- plugins/_bundled/sirsoft-pay_nhnkcp/tests/Unit/Listeners/RegisterEasyPayMethodsListenerTest.php
|
||||
- plugins/_bundled/sirsoft-pay_nhnkcp/tests/Unit/Upgrades/BackfillEasyPayPgProviderTest.php
|
||||
- plugins/_bundled/sirsoft-pay_nicepayments/tests/Unit/Listeners/RegisterEasyPayMethodsListenerTest.php
|
||||
@@ -30,12 +31,18 @@ axes:
|
||||
# 카탈로그가 능력을 선언했는가 (플러그인이 신규 키를 안 쓰는 하위호환 경로)
|
||||
capability_declared: [declared, undeclared]
|
||||
# 능력별 판정 표면
|
||||
capability: [needs_pg, label, refund_method, pg_locked]
|
||||
capability: [needs_pg, label, refund_method, pg_locked, brand_mark]
|
||||
|
||||
exclusions:
|
||||
- method_kind: builtin
|
||||
capability_declared: undeclared
|
||||
reason: "builtin 은 코어가 enum 에서 능력을 항상 파생하므로 '미선언' 상태가 존재하지 않는다"
|
||||
- method_kind: builtin
|
||||
capability: brand_mark
|
||||
reason: "브랜드 마크는 PG 플러그인이 등록한 간편결제 수단의 표현이다. 코어 8종은 브랜드가 아니라 결제 형태(카드·가상계좌 등)이므로 brand_mark 를 선언하지 않는다"
|
||||
- capability: brand_mark
|
||||
capability_declared: undeclared
|
||||
reason: "brand_mark 는 다른 능력과 달리 미선언 시 파생 경로가 없다 — 관리자 화면은 공급 플러그인이 직접 주입하는 배지로만 그리므로, 미선언은 곧 '배지 없음(기본 아이콘 유지)' 이고 판정할 후속 효과가 존재하지 않는다"
|
||||
|
||||
# 후속 효과 체인 — 능력 오판이 낳는 실패들
|
||||
effects:
|
||||
@@ -57,6 +64,8 @@ effects:
|
||||
- admin_shows_pg_locked_badge # 확장 결제수단이 "PG 고정 · {PG명}" 배지로 표시
|
||||
- admin_hides_pg_select_for_locked # PG 고정 수단은 PG 선택 셀렉트 미노출
|
||||
- admin_shows_pg_select_for_unlocked # 일반 PG 수단은 셀렉트 노출 (회귀 차단)
|
||||
- admin_shows_brand_mark # 간편결제 행의 기본 아이콘이 공급 플러그인의 브랜드 배지로 치환
|
||||
- admin_rebinds_brand_mark_after_tab_return # 탭 전환(replaceState) 후 복귀해도 배지 재주입 — 새로고침 불요
|
||||
# 기존 설치 환경
|
||||
- saved_null_pg_provider_self_healed # 저장된 pg_provider=null 이 정의값으로 덮임
|
||||
- upgrade_step_backfills_settings_file # upgrade step 이 저장 파일 자체도 정정 (멱등)
|
||||
|
||||
-1
@@ -22,7 +22,6 @@ effects:
|
||||
test_files:
|
||||
- modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/order-admin-list-context-round-trip.spec.ts
|
||||
- modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/admin-list-context-round-trip.test.ts
|
||||
- .claude/scripts/audit/__tests__/layout-list-context-navigate-merge-query.test.cjs
|
||||
|
||||
validation:
|
||||
note: |
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ feature: 리뷰 목록의 첨부 이미지 계약 고정 (#518 / 공개 #76)
|
||||
description: |
|
||||
#518 원안은 리뷰 목록에서 `images` 를 빼고 대표 1장 + `image_count` 로 줄이려 했다.
|
||||
소비처를 재실측한 결과 그 판단이 빗나갔다 — 관리자·공개 두 목록 모두 첨부 이미지를
|
||||
실제로 그린다. 착수하지 않는다 (PO 결정, 2026-08-05).
|
||||
실제로 그린다. 착수하지 않는다 (프로젝트 결정, 2026-08-05).
|
||||
|
||||
이 매니페스트는 "줄였다" 를 기록하는 것이 아니라 **줄일 수 없다는 사실을 고정**한다.
|
||||
제거 시 조용히 깨지기 때문이다: 썸네일이 `(review.images ?? []).length > 0` 가드 뒤에
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.2] - 2026-07-16
|
||||
## [1.0.2] - 2026-08-10
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 페이지 모듈 Playwright E2E 설정.
|
||||
*
|
||||
* 코어 `playwright.config.ts` 및 이커머스 모듈 설정과 동일한 base URL 해석 우선순위를 따른다 —
|
||||
* 모듈도 활성 호스트가 가변(개발자/CI/PO 환경별로 다른 도메인)이므로 하드코딩 회피.
|
||||
* 모듈도 활성 호스트가 가변(개발자/CI/운영 환경별로 다른 도메인)이므로 하드코딩 회피.
|
||||
*
|
||||
* Base URL 해석:
|
||||
* 1. PLAYWRIGHT_BASE_URL 환경변수 (CI/명시적 오버라이드)
|
||||
|
||||
@@ -19,7 +19,6 @@ effects:
|
||||
test_files:
|
||||
- modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-admin-list-context-round-trip.spec.ts
|
||||
- modules/_bundled/sirsoft-page/resources/js/__tests__/layouts/admin-list-context-round-trip.test.ts
|
||||
- .claude/scripts/audit/__tests__/layout-list-context-navigate-merge-query.test.cjs
|
||||
|
||||
validation:
|
||||
note: |
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [0.1.1] - 2026-07-22
|
||||
## [0.1.1] - 2026-08-10
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.1] - 2026-07-22
|
||||
## [1.0.1] - 2026-08-10
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.2] - 2026-07-30
|
||||
## [1.0.2] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -534,7 +534,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "항목 리스트 (이슈 #430 재설계) — 상태 배지 + 동의/철회 버튼 방식. 필수=상태 배지 '항상 적용' + 잠금 버튼(액션 없음), 선택=상태 배지(동의함/거부함/미설정) + 동의·철회 버튼(확인 모달 경유). 동의일 컬럼은 사용자 화면에서 제거(동의한 항목만 라벨 아래 날짜 표시, 감사 로그 전체는 관리자 화면에만 존재). 초기 설계였던 on/off 스위치는 user 템플릿 스타일 회귀 + PO 지시로 배지+버튼 방식으로 대체됨(2026-07-21).",
|
||||
"comment": "항목 리스트 (이슈 #430 재설계) — 상태 배지 + 동의/철회 버튼 방식. 필수=상태 배지 '항상 적용' + 잠금 버튼(액션 없음), 선택=상태 배지(동의함/거부함/미설정) + 동의·철회 버튼(확인 모달 경유). 동의일 컬럼은 사용자 화면에서 제거(동의한 항목만 라벨 아래 날짜 표시, 감사 로그 전체는 관리자 화면에만 존재). 초기 설계였던 on/off 스위치는 user 템플릿 스타일 회귀 + 프로젝트 지시로 배지+버튼 방식으로 대체됨(2026-07-21).",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(gdprMeConsents?.data?.consents ?? []).length > 0}}",
|
||||
|
||||
+1
-1
@@ -564,7 +564,7 @@ describe('admin/plugin_settings.json — 3 카드 (operator / cookie_banner / au
|
||||
|
||||
/**
|
||||
* sequence handler 구조 회귀 가드 — actions 배열이 params 안에 잘못 중첩되어
|
||||
* (CLAUDE.md 절대 금지 패턴) 있었던 결함. 엔진(ActionDispatcher.handleSequence)이
|
||||
* (레이아웃 JSON 금지 패턴) 있었던 결함. 엔진(ActionDispatcher.handleSequence)이
|
||||
* 두 위치를 모두 인식하는 fallback을 갖고 있어 동작 자체는 정상이었지만, 정석
|
||||
* 위치(actions가 액션 객체의 top-level)로 바로잡는다.
|
||||
*/
|
||||
|
||||
+2
-2
@@ -312,7 +312,7 @@ describe('extensions/mypage_privacy_tab.json — 마이페이지 GDPR 동의 매
|
||||
* @effects mypage_status_date_line_consented_only
|
||||
*/
|
||||
it('이슈 #430/#509 — 동의·철회 이력이 있는 항목만 날짜 줄 노출 (서버가 status_at_formatted 제공)', () => {
|
||||
// PO 결정(2026-07-20) + 이슈 #509 16번 갱신: 날짜는 "동의한 항목" + "철회 이력이 있는 항목".
|
||||
// 프로젝트 결정(2026-07-20) + 이슈 #509 16번 갱신: 날짜는 "동의한 항목" + "철회 이력이 있는 항목".
|
||||
// 거부·신규 미설정은 서버가 status_at_formatted 를 null 로 내려주므로 날짜 줄이 렌더되지 않는다.
|
||||
// 프론트는 status_label && status_at_formatted 가 둘 다 있을 때만 날짜 줄을 그리며, 값 자체는
|
||||
// 서버가 상태별(동의일/철회일)로 계산해 내려주므로 프론트 조건식은 상태 무관 동일하게 유지된다.
|
||||
@@ -328,7 +328,7 @@ describe('extensions/mypage_privacy_tab.json — 마이페이지 GDPR 동의 매
|
||||
* @effects mypage_status_date_line_revoked_color_distinction
|
||||
*/
|
||||
it('이슈 #509 16번 — 동의·철회 날짜 줄을 서로 다른 색으로 균형있게 강조 (동의=초록/철회=빨강, 배지·관리자 이력 화면과 동일 톤)', () => {
|
||||
// 철회만 강조되고 동의는 밋밋해 보인다는 PO 피드백 반영 — 동의도 철회와 대등하게 강조한다.
|
||||
// 철회만 강조되고 동의는 밋밋해 보인다는 사용성 피드백 반영 — 동의도 철회와 대등하게 강조한다.
|
||||
// status === 'revoked' 면 text-red-600/dark:text-red-400, 그 외(동의)는 text-green-600/dark:text-green-400.
|
||||
const text = serializeForSearch(section);
|
||||
expect(text).toContain("consent?.status === 'revoked'");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* GDPR 플러그인 Playwright E2E 설정.
|
||||
*
|
||||
* 코어 `playwright.config.ts` 와 동일한 base URL 해석 우선순위를 따른다 — 플러그인도 활성 호스트가
|
||||
* 가변(개발자/CI/PO 환경별로 다른 도메인)이므로 하드코딩 회피.
|
||||
* 가변(개발자/CI/운영 환경별로 다른 도메인)이므로 하드코딩 회피.
|
||||
*
|
||||
* Base URL 해석:
|
||||
* 1. PLAYWRIGHT_BASE_URL 환경변수 (CI/명시적 오버라이드)
|
||||
@@ -76,6 +76,12 @@ export default defineConfig({
|
||||
],
|
||||
use: {
|
||||
baseURL: resolveBaseUrl(),
|
||||
// spec 이 한국어 화면 문구를 단언하므로 로케일을 고정한다.
|
||||
// 로케일 우선순위는 localStorage g7_locale → 서버 응답값 → 'ko' 이고, 서버값은 미인증
|
||||
// 요청에서 Accept-Language 로 결정된다(SetLocale 미들웨어). Playwright 의 locale 옵션이
|
||||
// 그 헤더를 만들므로, 지정하지 않으면 첫 페이지 로드가 en-US 로 나가 화면이 영어로 렌더되고
|
||||
// 엔진이 그 값을 localStorage 에 저장해 이후 인증해도 세션 전체가 영어로 고정된다.
|
||||
locale: 'ko-KR',
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
ignoreHTTPSErrors: true,
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* E2E: 관리자 환경설정 > 쿠키 배너 > 정책 버전 > 보기 — 본문 페이지 링크 (#509 체크리스트 12번)
|
||||
* + 관리자 환경설정 카드 제목 색상 회귀 가드 (PO 스크린샷 발견)
|
||||
* + 관리자 환경설정 카드 제목 색상 회귀 가드 (브라우저 스크린샷으로 발견)
|
||||
* + 정책 버전 이력 토글 sequence handler 회귀 가드
|
||||
*
|
||||
* @scenario admin_gdpr_policy_version_snapshot_open_policy_page_link
|
||||
@@ -13,11 +13,11 @@
|
||||
* 모달의 본문 페이지 링크 href 를 검증할 수 있다.
|
||||
*
|
||||
* 또한 카드 제목(H3)이 에러/경고 전용 시맨틱(text-error-strong, red 계열)을 오용해
|
||||
* "운영 주체" 등 일반 카드 제목이 빨간색으로 표시되던 결함을 PO 가 스크린샷으로 발견해
|
||||
* "운영 주체" 등 일반 카드 제목이 빨간색으로 표시되던 결함을 브라우저 스크린샷으로 발견해
|
||||
* section-heading-md(중립 톤)로 수정 — 실제 렌더링된 색상을 브라우저에서 검증한다.
|
||||
*
|
||||
* 정책 버전 이력 토글 버튼의 sequence handler 가 actions 를 params 안에 잘못 중첩하고
|
||||
* 있던 결함(CLAUDE.md 절대 금지 패턴)도 함께 발견 — 엔진이 두 위치 모두 인식하는
|
||||
* 있던 결함(레이아웃 JSON 금지 패턴)도 함께 발견 — 엔진이 두 위치 모두 인식하는
|
||||
* fallback을 갖고 있어 동작 자체는 정상이었으나 정석 위치(top-level)로 바로잡았다.
|
||||
* 실제 클릭 시 이력 표가 펼쳐지는지(=sequence 의 setState 가 실행됐는지)를 검증한다.
|
||||
*
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
*
|
||||
* 배경: `_global.gdprBannerDismissedFor` 가 "닫힘 여부"(boolean)가 아니라 "누가 닫았는지"
|
||||
* (사용자 식별자)를 저장하도록 수정되기 전에는, 게스트로 배너를 닫은 뒤 동의 이력이 없는
|
||||
* 다른 계정으로 로그인해도 배너가 계속 숨겨져 있는 결함이 있었다 (PO 가 스크린샷으로 재현·지적).
|
||||
* 다른 계정으로 로그인해도 배너가 계속 숨겨져 있는 결함이 있었다 (브라우저 스크린샷으로 재현·지적).
|
||||
* 현재는 cookie_banner.json 의 노출 조건이 `_global.gdprBannerDismissedFor !== (사용자 식별자)`
|
||||
* 로 비교하므로, 로그인으로 사용자가 바뀌면(게스트 uuid → 회원 uuid) 다시 노출되어야 한다.
|
||||
*
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ class GdprUserConsentResourceTest extends PluginTestCase
|
||||
|
||||
public function test_rejected_optional_maps_to_rejected_status_with_no_date_label(): void
|
||||
{
|
||||
// PO 결정(2026-07-20) — 날짜/라벨은 "동의한 항목만" 노출. 거부는 상태만 rejected 로 내려주고
|
||||
// 프로젝트 결정(2026-07-20) — 날짜/라벨은 "동의한 항목만" 노출. 거부는 상태만 rejected 로 내려주고
|
||||
// status_label·status_at_formatted 는 null (프론트가 날짜 줄을 렌더하지 않음).
|
||||
$consent = new GdprUserConsent([
|
||||
'consent_key' => 'cookie_analytics',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.1] - 2026-07-29
|
||||
## [1.0.1] - 2026-08-10
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.1.0] - 2026-08-06
|
||||
## [1.1.0] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- 이커머스 관리자 주문설정에서 다른 탭에 갔다 돌아오면 간편결제 항목의 브랜드 로고가 사라지고 회색 기본 아이콘으로 남던 문제를 수정했습니다. 화면을 새로고침해야만 다시 표시됐습니다.
|
||||
- 결제 취소 기록의 환불 금액에 원화 단위가 고정 표기되던 문제를 수정했습니다. 해외 결제(엔화 등) 취소도 실제 결제 통화로 표시됩니다.
|
||||
|
||||
- 쇼핑몰 주소를 기본값이 아닌 값으로 바꾸거나 주소 없이 운영하는 상점에서, 결제를 마치거나 결제에 실패한 구매자가 존재하지 않는 페이지로 이동하던 문제를 수정했습니다. 이동 주소가 기본 주소로 고정돼 있었기 때문이며, 이제 상점 주소 설정을 따라갑니다. 이미 운영 중인 사이트도 업데이트 시 자동으로 반영되며, 이동 주소를 직접 입력해 두었다면 그 값은 그대로 유지됩니다. (#85 @hwaryeon1234 님께서 제보해주셨습니다.)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"identifier": "sirsoft-pay_kginicis",
|
||||
"version": "1.0.2",
|
||||
"version": "1.1.0",
|
||||
"components": {
|
||||
"basic": [],
|
||||
"composite": [],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(function(){"use strict";const x="kginicis_pay_form_",R="__sirsoftKginicisReloadStandardPaySdk",w="__sirsoftKginicisMobilePaymentReturnPending";function oe(){if(typeof document>"u")return 0;const e=document.querySelectorAll(`form[id^="${x}"]`);return e.forEach(t=>t.remove()),e.length}function qe(){typeof window>"u"||(window[R]=!0)}function We(){if(typeof window>"u")return!1;const e=window,t=e[R]===!0;return delete e[R],t}function Ve(e){typeof window>"u"||typeof document>"u"||(window.INIStdPay=void 0,document.querySelectorAll(`script[src="${e}"], script[src*="/INIStdPay_third-party.js"]`).forEach(t=>t.remove()))}function ze(e=Date.now()){if(!(typeof window>"u"))try{window.sessionStorage.setItem(w,String(e))}catch{window[w]=e}}function M(){if(!(typeof window>"u")){try{window.sessionStorage.removeItem(w)}catch{}delete window[w]}}function ie(e=Date.now()){if(typeof window>"u")return!1;let t=null;try{const n=window.sessionStorage.getItem(w);t=n?Number(n):null}catch{t=null}if(!Number.isFinite(t)){const n=window[w];t=typeof n=="number"?n:Number(n)}return!Number.isFinite(t)||e-t>18e5?(M(),!1):!0}const N="sirsoft-pay_kginicis",Xe=N,Ze="payment-window-closed",ae="__sirsoftKginicisPaymentCloseListenerInstalled",L="__sirsoftKginicisActiveStandardPaymentCloseContext",se=20,Qe=100,O={info:(...e)=>console.info(`[${N}]`,...e),warn:(...e)=>console.warn(`[${N}]`,...e)};function et(e){if(!e||typeof e!="object")return!1;const t=e;return t.source===Xe&&t.type===Ze}function $(){return/\/shop\/checkout\/?$/.test(window.location.pathname)}function P(){return window}function tt(){const e=P()[L];return!e||typeof e!="object"?null:e}function nt(e){return/^https?:\/\//i.test(e)||e.startsWith("/api/")?e:e.startsWith("/plugins/")?`/api${e}`:e.startsWith("plugins/")?`/api/${e}`:e}function rt(e){e.closeReportUrl&&(P()[L]={...e,reported:!1})}function ce(){delete P()[L]}function de(){ce()}async function ot(e="payment-window-closed"){const t=tt();if(!t||t.reported||!$())return;t.reported=!0;const n={oid:t.oid,price:t.price,buyer_email:t.buyer_email??"",buyer_phone:t.buyer_phone??"",payment_method:t.payment_method??"",reason:e};try{const r=window.G7Core?.api;typeof r?.post=="function"?await r.post(t.closeReportUrl,n):await fetch(nt(t.closeReportUrl),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n),keepalive:!0})}catch(r){O.warn("failed to report KG payment window close",{reason:e,error:r})}finally{ce()}}function le(e="payment-window-closed",t=!0){if(!$())return!1;const r=window.G7Core?.state?.setLocal;return typeof r!="function"?(t&&O.warn("G7Core.state.setLocal not available while resetting payment submit state"),!1):(oe(),qe(),r({isSubmittingOrder:!1}),O.info("checkout submit state reset after KG payment close",{reason:e}),!0)}function it(e,t=!1){let n=0;const r=()=>{if(n++,!(t&&!ie())){if(le(e,n>=se)){t&&M();return}!$()||n>=se||window.setTimeout(r,Qe)}};r()}function I(e){ie()&&it(e,!0)}function at(){if(typeof window>"u")return;const e=P();e[ae]||(window.addEventListener("message",t=>{t.origin===window.location.origin&&et(t.data)&&(ot(t.data.reason),le(t.data.reason))}),window.addEventListener("pagehide",()=>{de()}),window.addEventListener("beforeunload",()=>{de()}),window.addEventListener("pageshow",t=>{I(t.persisted?"mobile-payment-bfcache-return":"mobile-payment-page-show")}),window.addEventListener("focus",()=>{I("mobile-payment-window-focus")}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&I("mobile-payment-visibility-return")}),e[ae]=!0,I("mobile-payment-listener-installed"))}function ue(){if(typeof navigator>"u")return!1;const e=navigator;if(e.userAgentData?.mobile!==void 0)return e.userAgentData.mobile;const t=(e.userAgent||"").toLowerCase(),n=((e.userAgentData?.platform??e.platform)||"").toLowerCase();if(/android|iphone|ipad|ipod|windows phone|iemobile|blackberry|opera mini|mobile safari/.test(t)||/iphone|ipad|ipod|ios/.test(n))return!0;const r=e.maxTouchPoints??0;return/macintosh|mac os x/.test(t)&&r>1}function st(e){return new Promise((t,n)=>{if(document.querySelector(`script[src="${e}"]`)){t();return}const r=document.createElement("script");r.src=e,r.async=!0,r.onload=()=>t(),r.onerror=()=>n(new Error(`Failed to load script: ${e}`)),document.head.appendChild(r)})}function me(e,t){if(!t.startsWith("kginicis_"))return e;const n=new URL(e,window.location.origin);return n.searchParams.set("selectedPaymentMethod",t),n.toString()}function ct(e){const t=(e??"").trim().toUpperCase();return t===""||t==="WON"?"KRW":t}function fe(e,t,n="utf-8",r){const o=document.createElement("form");r&&(o.id=r),o.method="POST",o.action=e,o.acceptCharset=n,o.style.display="none";for(const[i,s]of Object.entries(t)){const a=document.createElement("input");a.type="hidden",a.name=i,a.value=s,o.appendChild(a)}document.body.appendChild(o),o.submit()}const dt={card:"Card",vbank:"VBank",bank:"DirectBank",phone:"HPP",kginicis_samsung_pay:"onlyssp",kginicis_naverpay:"onlynaverpay",kginicis_lLine truncated
|
||||
(function(){"use strict";const x="kginicis_pay_form_",R="__sirsoftKginicisReloadStandardPaySdk",w="__sirsoftKginicisMobilePaymentReturnPending";function ie(){if(typeof document>"u")return 0;const e=document.querySelectorAll(`form[id^="${x}"]`);return e.forEach(t=>t.remove()),e.length}function qe(){typeof window>"u"||(window[R]=!0)}function We(){if(typeof window>"u")return!1;const e=window,t=e[R]===!0;return delete e[R],t}function Ve(e){typeof window>"u"||typeof document>"u"||(window.INIStdPay=void 0,document.querySelectorAll(`script[src="${e}"], script[src*="/INIStdPay_third-party.js"]`).forEach(t=>t.remove()))}function ze(e=Date.now()){if(!(typeof window>"u"))try{window.sessionStorage.setItem(w,String(e))}catch{window[w]=e}}function M(){if(!(typeof window>"u")){try{window.sessionStorage.removeItem(w)}catch{}delete window[w]}}function ae(e=Date.now()){if(typeof window>"u")return!1;let t=null;try{const n=window.sessionStorage.getItem(w);t=n?Number(n):null}catch{t=null}if(!Number.isFinite(t)){const n=window[w];t=typeof n=="number"?n:Number(n)}return!Number.isFinite(t)||e-t>18e5?(M(),!1):!0}const N="sirsoft-pay_kginicis",Xe=N,Ze="payment-window-closed",se="__sirsoftKginicisPaymentCloseListenerInstalled",L="__sirsoftKginicisActiveStandardPaymentCloseContext",ce=20,Qe=100,O={info:(...e)=>console.info(`[${N}]`,...e),warn:(...e)=>console.warn(`[${N}]`,...e)};function et(e){if(!e||typeof e!="object")return!1;const t=e;return t.source===Xe&&t.type===Ze}function $(){return/\/shop\/checkout\/?$/.test(window.location.pathname)}function P(){return window}function tt(){const e=P()[L];return!e||typeof e!="object"?null:e}function nt(e){return/^https?:\/\//i.test(e)||e.startsWith("/api/")?e:e.startsWith("/plugins/")?`/api${e}`:e.startsWith("plugins/")?`/api/${e}`:e}function rt(e){e.closeReportUrl&&(P()[L]={...e,reported:!1})}function de(){delete P()[L]}function le(){de()}async function ot(e="payment-window-closed"){const t=tt();if(!t||t.reported||!$())return;t.reported=!0;const n={oid:t.oid,price:t.price,buyer_email:t.buyer_email??"",buyer_phone:t.buyer_phone??"",payment_method:t.payment_method??"",reason:e};try{const r=window.G7Core?.api;typeof r?.post=="function"?await r.post(t.closeReportUrl,n):await fetch(nt(t.closeReportUrl),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n),keepalive:!0})}catch(r){O.warn("failed to report KG payment window close",{reason:e,error:r})}finally{de()}}function ue(e="payment-window-closed",t=!0){if(!$())return!1;const r=window.G7Core?.state?.setLocal;return typeof r!="function"?(t&&O.warn("G7Core.state.setLocal not available while resetting payment submit state"),!1):(ie(),qe(),r({isSubmittingOrder:!1}),O.info("checkout submit state reset after KG payment close",{reason:e}),!0)}function it(e,t=!1){let n=0;const r=()=>{if(n++,!(t&&!ae())){if(ue(e,n>=ce)){t&&M();return}!$()||n>=ce||window.setTimeout(r,Qe)}};r()}function I(e){ae()&&it(e,!0)}function at(){if(typeof window>"u")return;const e=P();e[se]||(window.addEventListener("message",t=>{t.origin===window.location.origin&&et(t.data)&&(ot(t.data.reason),ue(t.data.reason))}),window.addEventListener("pagehide",()=>{le()}),window.addEventListener("beforeunload",()=>{le()}),window.addEventListener("pageshow",t=>{I(t.persisted?"mobile-payment-bfcache-return":"mobile-payment-page-show")}),window.addEventListener("focus",()=>{I("mobile-payment-window-focus")}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&I("mobile-payment-visibility-return")}),e[se]=!0,I("mobile-payment-listener-installed"))}function me(){if(typeof navigator>"u")return!1;const e=navigator;if(e.userAgentData?.mobile!==void 0)return e.userAgentData.mobile;const t=(e.userAgent||"").toLowerCase(),n=((e.userAgentData?.platform??e.platform)||"").toLowerCase();if(/android|iphone|ipad|ipod|windows phone|iemobile|blackberry|opera mini|mobile safari/.test(t)||/iphone|ipad|ipod|ios/.test(n))return!0;const r=e.maxTouchPoints??0;return/macintosh|mac os x/.test(t)&&r>1}function st(e){return new Promise((t,n)=>{if(document.querySelector(`script[src="${e}"]`)){t();return}const r=document.createElement("script");r.src=e,r.async=!0,r.onload=()=>t(),r.onerror=()=>n(new Error(`Failed to load script: ${e}`)),document.head.appendChild(r)})}function fe(e,t){if(!t.startsWith("kginicis_"))return e;const n=new URL(e,window.location.origin);return n.searchParams.set("selectedPaymentMethod",t),n.toString()}function ct(e){const t=(e??"").trim().toUpperCase();return t===""||t==="WON"?"KRW":t}function pe(e,t,n="utf-8",r){const o=document.createElement("form");r&&(o.id=r),o.method="POST",o.action=e,o.acceptCharset=n,o.style.display="none";for(const[i,s]of Object.entries(t)){const a=document.createElement("input");a.type="hidden",a.name=i,a.value=s,o.appendChild(a)}document.body.appendChild(o),o.submit()}const dt={card:"Card",vbank:"VBank",bank:"DirectBank",phone:"HPP",kginicis_samsung_pay:"onlyssp",kginicis_naverpay:"onlynaverpay",kginicis_lLine truncated
|
||||
<div class="row">
|
||||
<dt>${E(i.label)}</dt>
|
||||
<dd>${E(i.value)}</dd>
|
||||
@@ -87,5 +87,5 @@
|
||||
<div class="actions"><button type="button" onclick="window.print()">인쇄</button></div>
|
||||
</main>
|
||||
</body>
|
||||
</html>`),t.document.close()}function E(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}const D="sirsoft-pay_kginicis",we="__kginicisOrderShowInjectorInstalled",K="kginicis-mp-receipt-row",be=/^(?:\/mypage\/orders\/([^/]+)|\/shop\/guest\/orders\/([^/]+))$/,G=new Map;function Tt(e){try{const n=window.G7Core?.getState,i=n?.()?.currentDataContext?.order?.data;return i&&(i.order_number===e||i.id!==void 0&&String(i.id)===e)?i:null}catch{return null}}function xt(){const e=document.getElementById("order_payment_info_panel");if(e)return Array.from(e.children).find(r=>r.className?.includes("space-y"))??e;const t=Array.from(document.querySelectorAll("h3")).find(r=>{const o=r.textContent?.trim()??"";return o.includes("결제 정보")||/^Payment Information$/i.test(o)});if(!t)return null;const n=t.parentElement?.parentElement;return n?Array.from(n.children).find(r=>r.className?.includes("space-y"))??n:null}function Rt(e,t){if(!t)return!1;const n=Array.from(e.querySelectorAll("div"));for(const r of n){const o=Array.from(r.children).filter(a=>a instanceof HTMLElement&&a.tagName==="SPAN");if(o.length<2)continue;const i=o[0].textContent?.trim();if(i!=="결제 방법"&&i!=="결제수단"&&i!=="Payment Method")continue;const s=o[o.length-1];return s.textContent?.trim()===t?(r.dataset.kginicisPaymentMethodRow="true",!0):(s.textContent=t,s.dataset.kginicisPaymentMethodPatched="true",r.dataset.kginicisPaymentMethodRow="true",!0)}return!1}function Mt(e,t){const n=document.createElement("div");n.id=K,n.className="flex items-center justify-between",n.dataset.kginicisReceiptRow="mypage-order";const r=document.createElement("span");r.className="text-gray-500 dark:text-gray-400 text-sm",r.textContent=vt(t);const o=document.createElement("button");return o.type="button",o.dataset.kginicisReceiptButton="mypage-order",o.className="inline-flex items-center gap-1 text-sm text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-50",o.textContent=A(t),o.addEventListener("click",async()=>{o.disabled=!0,o.textContent="로딩 중...";const i=await v(e);o.disabled=!1,o.textContent=A(i??t),C(i)&&ge(i)}),n.appendChild(r),n.appendChild(o),n}async function Nt(e,t){const n=Tt(e),r=n?.payment;if(r&&(r.pg_provider!=="kginicis"||!r.transaction_id))return!0;const o=n?.order_number??(t?e:null);if(!o)return!1;if(!t&&n&&!r)return!0;const i=xt();if(!i)return!1;const{status:s,info:a}=await he(o);if(!t&&s===404)return!0;const c=r?.payment_status==="paid",u=a?.receipt_type==="cbt_confirmation";if(r&&!c&&!u)return!0;if(!r&&!C(a))return!1;const d=Rt(i,a?.payment_method_display_label);return!document.getElementById(K)&&C(a)&&(i.appendChild(Mt(o,a)),console.info(`[${D}] receipt button injected on mypage order show`)),d&&console.info(`[${D}] payment method display patched on mypage order show`),!!(document.getElementById(K)||d)}function Lt(e,t){if(G.has(e))return;let n=0;const r=window.setInterval(()=>{n++,Nt(e,t).then(o=>{(o||n>=30)&&(window.clearInterval(r),G.delete(e))})},400);G.set(e,r)}function Ot(){const e=location.pathname.match(be);if(e){const t=e[1],n=e[2],r=t??n;r&&Lt(r,n!==void 0)}}function $t(){if(typeof window>"u")return;const e=window;if(e[we])return;e[we]=!0,console.info(`[${D}] mypage order show injector installed`);let t=null;const n=(a=1500)=>{t!==null&&window.clearTimeout(t),t=window.setTimeout(()=>{t=null,Ot()},a)};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>n()):n();const r=history.pushState.bind(history);history.pushState=(...a)=>{r(...a),n(600)};const o=history.replaceState.bind(history);history.replaceState=(...a)=>{o(...a),n(600)},window.addEventListener("popstate",()=>n(500));const i=document.getElementById("app")??document.body;new MutationObserver(()=>{be.test(location.pathname)&&n(250)}).observe(i,{childList:!0,subtree:!0})}const j="sirsoft-pay_kginicis",ke="__kginicisAdminPaymentDisplayInjectorInstalled",Dt=/^\/admin\/ecommerce\/orders\/([^/]+)$/;function Kt(){return localStorage.getItem("auth_token")}async function Gt(e){const t=Kt();if(!t)return null;try{const n=await fetch(`/api/plugins/${j}/admin/orders/${e}/transaction-status`,{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}});if(!n.ok)return null;const r=await n.json();return("data"in r?r.data:r)??null}catch{return null}}function jt(e){return Array.from(e.children).filter(t=>t instanceof HTMLElement)}function Yt(e,t){let n=!1;const r=Array.from(e.querySelectorAll("div"));for(const o of r){const i=jt(o),s=i.findIndex(l=>l.textContent?.trim()==="결제수단");if(s<0)continue;const a=i.slice(s+1).find(l=>l.textContent?.trim()&&l.textContent.trim()!=="결제수단");if(!a)continue;const c=i.slice(s+2).find(l=>/^\(.+\)$/.test(l.textContent?.trim()??"")),u=c?.textContent?.trim().replace(/^\(|\)$/g,"")??"",d=u&&t.endsWith(")")?t.replace(/\)$/,`, ${u})`):t;a.textContent?.trim()!==d&&(a.textContent=d,a.dataset.kginicisPaymentMethodPatched="true"),o.dataLine truncated
|
||||
</html>`),t.document.close()}function E(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}const D="sirsoft-pay_kginicis",be="__kginicisOrderShowInjectorInstalled",K="kginicis-mp-receipt-row",ke=/^(?:\/mypage\/orders\/([^/]+)|\/shop\/guest\/orders\/([^/]+))$/,G=new Map;function Tt(e){try{const n=window.G7Core?.getState,i=n?.()?.currentDataContext?.order?.data;return i&&(i.order_number===e||i.id!==void 0&&String(i.id)===e)?i:null}catch{return null}}function xt(){const e=document.getElementById("order_payment_info_panel");if(e)return Array.from(e.children).find(r=>r.className?.includes("space-y"))??e;const t=Array.from(document.querySelectorAll("h3")).find(r=>{const o=r.textContent?.trim()??"";return o.includes("결제 정보")||/^Payment Information$/i.test(o)});if(!t)return null;const n=t.parentElement?.parentElement;return n?Array.from(n.children).find(r=>r.className?.includes("space-y"))??n:null}function Rt(e,t){if(!t)return!1;const n=Array.from(e.querySelectorAll("div"));for(const r of n){const o=Array.from(r.children).filter(a=>a instanceof HTMLElement&&a.tagName==="SPAN");if(o.length<2)continue;const i=o[0].textContent?.trim();if(i!=="결제 방법"&&i!=="결제수단"&&i!=="Payment Method")continue;const s=o[o.length-1];return s.textContent?.trim()===t?(r.dataset.kginicisPaymentMethodRow="true",!0):(s.textContent=t,s.dataset.kginicisPaymentMethodPatched="true",r.dataset.kginicisPaymentMethodRow="true",!0)}return!1}function Mt(e,t){const n=document.createElement("div");n.id=K,n.className="flex items-center justify-between",n.dataset.kginicisReceiptRow="mypage-order";const r=document.createElement("span");r.className="text-gray-500 dark:text-gray-400 text-sm",r.textContent=vt(t);const o=document.createElement("button");return o.type="button",o.dataset.kginicisReceiptButton="mypage-order",o.className="inline-flex items-center gap-1 text-sm text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-50",o.textContent=A(t),o.addEventListener("click",async()=>{o.disabled=!0,o.textContent="로딩 중...";const i=await v(e);o.disabled=!1,o.textContent=A(i??t),C(i)&&we(i)}),n.appendChild(r),n.appendChild(o),n}async function Nt(e,t){const n=Tt(e),r=n?.payment;if(r&&(r.pg_provider!=="kginicis"||!r.transaction_id))return!0;const o=n?.order_number??(t?e:null);if(!o)return!1;if(!t&&n&&!r)return!0;const i=xt();if(!i)return!1;const{status:s,info:a}=await ge(o);if(!t&&s===404)return!0;const c=r?.payment_status==="paid",u=a?.receipt_type==="cbt_confirmation";if(r&&!c&&!u)return!0;if(!r&&!C(a))return!1;const d=Rt(i,a?.payment_method_display_label);return!document.getElementById(K)&&C(a)&&(i.appendChild(Mt(o,a)),console.info(`[${D}] receipt button injected on mypage order show`)),d&&console.info(`[${D}] payment method display patched on mypage order show`),!!(document.getElementById(K)||d)}function Lt(e,t){if(G.has(e))return;let n=0;const r=window.setInterval(()=>{n++,Nt(e,t).then(o=>{(o||n>=30)&&(window.clearInterval(r),G.delete(e))})},400);G.set(e,r)}function Ot(){const e=location.pathname.match(ke);if(e){const t=e[1],n=e[2],r=t??n;r&&Lt(r,n!==void 0)}}function $t(){if(typeof window>"u")return;const e=window;if(e[be])return;e[be]=!0,console.info(`[${D}] mypage order show injector installed`);let t=null;const n=(a=1500)=>{t!==null&&window.clearTimeout(t),t=window.setTimeout(()=>{t=null,Ot()},a)};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>n()):n();const r=history.pushState.bind(history);history.pushState=(...a)=>{r(...a),n(600)};const o=history.replaceState.bind(history);history.replaceState=(...a)=>{o(...a),n(600)},window.addEventListener("popstate",()=>n(500));const i=document.getElementById("app")??document.body;new MutationObserver(()=>{ke.test(location.pathname)&&n(250)}).observe(i,{childList:!0,subtree:!0})}const j="sirsoft-pay_kginicis",Ce="__kginicisAdminPaymentDisplayInjectorInstalled",Dt=/^\/admin\/ecommerce\/orders\/([^/]+)$/;function Kt(){return localStorage.getItem("auth_token")}async function Gt(e){const t=Kt();if(!t)return null;try{const n=await fetch(`/api/plugins/${j}/admin/orders/${e}/transaction-status`,{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}});if(!n.ok)return null;const r=await n.json();return("data"in r?r.data:r)??null}catch{return null}}function jt(e){return Array.from(e.children).filter(t=>t instanceof HTMLElement)}function Yt(e,t){let n=!1;const r=Array.from(e.querySelectorAll("div"));for(const o of r){const i=jt(o),s=i.findIndex(l=>l.textContent?.trim()==="결제수단");if(s<0)continue;const a=i.slice(s+1).find(l=>l.textContent?.trim()&&l.textContent.trim()!=="결제수단");if(!a)continue;const c=i.slice(s+2).find(l=>/^\(.+\)$/.test(l.textContent?.trim()??"")),u=c?.textContent?.trim().replace(/^\(|\)$/g,"")??"",d=u&&t.endsWith(")")?t.replace(/\)$/,`, ${u})`):t;a.textContent?.trim()!==d&&(a.textContent=d,a.dataset.kginicisPaymentMethodPatched="true"),o.dataLine truncated
|
||||
//# sourceMappingURL=plugin.iife.js.map
|
||||
+29
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
installAdminPaymentMethodBrandInjector,
|
||||
resetAdminPaymentMethodBrandInjectorForTests,
|
||||
syncRenderedAdminPaymentMethodBrands,
|
||||
} from '../adminPaymentMethodBrandInjector';
|
||||
@@ -84,4 +85,32 @@ describe('adminPaymentMethodBrandInjector', () => {
|
||||
expect(syncRenderedAdminPaymentMethodBrands()).toBe(false);
|
||||
expect(document.querySelector('[data-kginicis-admin-payment-brand-mark="true"]')).toBeNull();
|
||||
});
|
||||
|
||||
/**
|
||||
* 탭 전환 후 복귀 시 배지 재주입 (브라우저 실측으로 발견한 회귀).
|
||||
*
|
||||
* 환경설정 화면의 탭 전환은 pushState 가 아니라 **replaceState** 로 URL 만 바꾼다
|
||||
* (탭 왕복 1회에 push 0 / replace 2 실측). pushState/popstate 만 후킹하던 기존 구현은
|
||||
* 다른 탭에 갔다 돌아왔을 때 onRouteChange 가 발화하지 않아, 행은 다시 렌더되는데
|
||||
* 배지만 사라진 채로 남았다 — 사용자에게는 아이콘이 회색으로 되돌아간 것으로 보인다.
|
||||
*/
|
||||
it('replaceState 로 탭을 전환했다 돌아와도 배지를 재주입한다', async () => {
|
||||
installAdminPaymentMethodBrandInjector();
|
||||
expect(document.querySelectorAll('[data-kginicis-admin-payment-brand-mark="true"]').length).toBeGreaterThan(0);
|
||||
|
||||
// 다른 탭으로 이동 (replaceState) — 화면이 갈아끼워지며 배지가 사라진 상태를 재현
|
||||
window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=shipping');
|
||||
document.body.innerHTML = '';
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
|
||||
// 주문설정으로 복귀 (replaceState) + 행 재렌더
|
||||
document.body.innerHTML = desktopItem('naverpay', '네이버페이 (KG이니시스)', '네이버페이로 결제');
|
||||
window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=order_settings');
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
|
||||
expect(
|
||||
document.querySelectorAll('[data-kginicis-admin-payment-brand-mark="true"]').length,
|
||||
'replaceState 복귀 후 배지가 재주입되지 않았다',
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -265,6 +265,17 @@ export function installAdminPaymentMethodBrandInjector(): void {
|
||||
origPush(...args);
|
||||
window.setTimeout(() => onRouteChange(), 200);
|
||||
};
|
||||
|
||||
// replaceState 도 후킹한다 — 환경설정 화면의 탭 전환은 pushState 가 아니라
|
||||
// replaceState 로 URL 만 바꾼다(실측: 탭 왕복 1회에 push 0 / replace 2). pushState 만
|
||||
// 후킹하면 다른 탭에 갔다 돌아왔을 때 onRouteChange 가 발화하지 않아 배지가 사라진 채로
|
||||
// 남는다(행은 그대로 렌더되므로 화면상 아이콘만 회색으로 되돌아간다).
|
||||
const origReplace = history.replaceState.bind(history);
|
||||
history.replaceState = (...args: Parameters<typeof history.replaceState>) => {
|
||||
origReplace(...args);
|
||||
window.setTimeout(() => onRouteChange(), 200);
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', () => window.setTimeout(() => onRouteChange(), 200));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.1] - 2026-07-22
|
||||
## [1.0.1] - 2026-08-10
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- 이커머스 관리자 주문설정에서 다른 탭에 갔다 돌아오면 간편결제 항목의 브랜드 색상 배지가 사라지고 회색 기본 아이콘으로 남던 문제를 수정했습니다. 화면을 새로고침해야만 다시 표시됐습니다.
|
||||
- 관리자 주문 상세의 거래 조회에서 취소·환불 금액에 원화 단위가 고정 표기되던 문제를 수정했습니다. 이제 주문 시점 기준 통화로 표시됩니다.
|
||||
- 결제 취소 기록의 환불 금액 표기도 결제 통화를 따르도록 수정했습니다.
|
||||
- 쇼핑몰 주소를 기본값이 아닌 값으로 바꾸거나 주소 없이 운영하는 상점에서, 결제를 마치거나 결제에 실패한 구매자가 존재하지 않는 페이지로 이동하던 문제를 수정했습니다. 이동 주소가 기본 주소로 고정돼 있었기 때문이며, 이제 상점 주소 설정을 따라갑니다. 이미 운영 중인 사이트도 업데이트 시 자동으로 반영되며, 이동 주소를 직접 입력해 두었다면 그 값은 그대로 유지됩니다. (#85 @hwaryeon1234 님께서 제보해주셨습니다.)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"identifier": "sirsoft-pay_nhnkcp",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"components": {
|
||||
"basic": [],
|
||||
"composite": [],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(function(){"use strict";function Le(e){return/^https?:\/\//i.test(e)||e.startsWith("/api/")?e:e.startsWith("/plugins/")?`/api${e}`:e.startsWith("plugins/")?`/api/${e}`:e}function Oe(e){return e.trim().slice(0,160)}function Ke(e){if(e)return e.replace(/\/payment\/close-report$/,"/payment/retry")}async function ee(e,t,n=!1){const o=window.G7Core?.api;if(typeof o?.post=="function"){await o.post(e,t);return}const r=await fetch(Le(e),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),keepalive:n});if(!r.ok){let a=`NHN KCP payment request failed (${r.status})`;try{const s=await r.json(),i=Array.isArray(s?.errors?.message)?s.errors.message[0]:s?.error??s?.message;typeof i=="string"&&i.trim()!==""&&(a=i)}catch{}throw new Error(a)}}async function je(e){const t=Ke(e.closeReportUrl);t&&await ee(t,{oid:e.oid,price:Number(e.price),buyer_email:e.buyer_email??"",buyer_phone:e.buyer_phone??"",payment_method:e.payment_method??""})}async function Be(e,t="kcp-window-closed"){if(!e.closeReportUrl)return;const n={oid:e.oid,price:Number(e.price),buyer_email:e.buyer_email??"",buyer_phone:e.buyer_phone??"",payment_method:e.payment_method??"",reason:Oe(t)||"kcp-window-closed"};try{await ee(e.closeReportUrl,n,!0)}catch(o){console.warn("[sirsoft-pay_nhnkcp] failed to report payment window close",o)}}const De="nhnkcp_applepay";function S(e){return e===De}function x(){if(typeof navigator>"u")return!1;const e=navigator,t=(e.userAgent||"").toLowerCase(),n=((e.userAgentData?.platform??e.platform)||"").toLowerCase(),o=e.maxTouchPoints??0;return/iphone|ipad|ipod/.test(t)||/iphone|ipad|ipod|ios/.test(n)?!0:/macintosh|mac os x/.test(t)&&o>1}function M(){return((typeof document<"u"?document.documentElement.lang:"")||"").toLowerCase().startsWith("ko")?"애플페이는 IOS 기기에 모바일결제만 가능합니다.":"Apple Pay is available only for mobile payments on iOS devices."}function T(e){return(e??"KRW").trim().toUpperCase()||"KRW"}function He(e){return T(e)==="KRW"}function Fe(e){const t=T(e);return((typeof document<"u"?document.documentElement.lang:"")||"").toLowerCase().startsWith("ko")?`NHN KCP는 KRW 결제만 지원합니다. (${t})`:`NHN KCP supports KRW payments only. (${t})`}function qe(e){const t=Math.max(0,Math.round(Number(e.amount??0))),n=Math.min(Math.max(0,Math.round(Number(e.tax_free_amount??0))),t);if(n<=0)return{};const o=Math.max(0,Math.round(Number(e.tax_amount??0))),r=Math.max(0,Math.round(Number(e.vat_amount??0))),a=Math.max(0,t-n),s=a>0&&o>0&&r>0?Math.min(a,Math.round(a*(r/o))):0,i=a-s;return{tax_flag:"TG03",comm_tax_mny:String(i),comm_vat_mny:String(s),comm_free_mny:String(n)}}class R extends Error{constructor(t){super(t??"결제가 취소되었습니다."),this.name="KcpCancelledError"}}const Ue={card:"100000000000",bank_transfer:"010000000000",virtual_account:"001000000000",mobile:"000010000000",bank:"010000000000",vbank:"001000000000",phone:"000010000000"},te={nhnkcp_payco:{payco_direct:"Y"},nhnkcp_naverpay:{naverpay_direct:"Y"},nhnkcp_naverpay_point:{naverpay_direct:"Y",naverpay_point_direct:"Y"},nhnkcp_kakaopay:{kakaopay_direct:"Y"},nhnkcp_applepay:{applepay_direct:"Y"}};function Ye(e,t){const n=e.toLowerCase();return!t||!Object.prototype.hasOwnProperty.call(te,n)?{}:{param_opt_1:n,nhnkcp_easy_pay_method:n}}function Ge(e){if(!e.isConnected)return!0;const t=window.getComputedStyle(e);if(t.display==="none"||t.visibility==="hidden")return!0;const n=e.getBoundingClientRect();return n.width<=0||n.height<=0}function We(e,t){let n=!1,o,r;const a=()=>{n=!0,r&&(r.disconnect(),r=void 0),o!==void 0&&(window.clearInterval(o),o=void 0)},s=()=>{n||!Ge(e)||(a(),t())},i=document.body??document.documentElement;return i&&typeof MutationObserver<"u"&&(r=new MutationObserver(s),r.observe(i,{attributes:!0,attributeFilter:["class","hidden","style"],childList:!0,subtree:!0})),o=window.setInterval(s,300),a}function ze(){if(typeof navigator>"u")return!1;if(x())return!0;const e=navigator;if(e.userAgentData?.mobile!==void 0)return e.userAgentData.mobile;const t=(navigator.userAgent||"").toLowerCase();return!!(/android|iphone|ipod|windows phone|iemobile|blackberry|opera mini|mobile safari/.test(t)||(navigator.maxTouchPoints??0)>1&&!t.includes("windows")&&!t.includes("macintosh"))}async function Je(e,t){const{pgPaymentData:n,paymentMethod:o}=e.params||{};if(!n){console.error("[sirsoft-pay_nhnkcp] pgPaymentData is required");return}const r=o??n.payment_method??n.pay_method??"card",a=typeof r=="string"&&r.startsWith("nhnkcp_"),s=window.G7Core;let i=null;try{if(!He(n.currency)){const u=Fe(n.currency);s?.state?.setLocal?.({paymentErrorMessage:u,isSubmittingOrder:!1,paymentMethod:r}),s?.modal?.open?.("nhnkcp_payment_error_modal");return}if(S(r)&&!x()){const u=M();s?.state?.setLocal?.({paymentErrorMessage:u,isSubmittingOrder:!1,paymentMethod:r}),s?.modal?.open?.("nhnkcp_payment_error_modal");return}const c=await s.api.get("/modules/sirsoft-ecommerce/payments/client-config/nhnkcp");if(!c.data){console.error("[Line truncated
|
||||
(function(){"use strict";function Le(e){return/^https?:\/\//i.test(e)||e.startsWith("/api/")?e:e.startsWith("/plugins/")?`/api${e}`:e.startsWith("plugins/")?`/api/${e}`:e}function Oe(e){return e.trim().slice(0,160)}function Ke(e){if(e)return e.replace(/\/payment\/close-report$/,"/payment/retry")}async function te(e,t,n=!1){const o=window.G7Core?.api;if(typeof o?.post=="function"){await o.post(e,t);return}const r=await fetch(Le(e),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),keepalive:n});if(!r.ok){let a=`NHN KCP payment request failed (${r.status})`;try{const s=await r.json(),i=Array.isArray(s?.errors?.message)?s.errors.message[0]:s?.error??s?.message;typeof i=="string"&&i.trim()!==""&&(a=i)}catch{}throw new Error(a)}}async function je(e){const t=Ke(e.closeReportUrl);t&&await te(t,{oid:e.oid,price:Number(e.price),buyer_email:e.buyer_email??"",buyer_phone:e.buyer_phone??"",payment_method:e.payment_method??""})}async function Be(e,t="kcp-window-closed"){if(!e.closeReportUrl)return;const n={oid:e.oid,price:Number(e.price),buyer_email:e.buyer_email??"",buyer_phone:e.buyer_phone??"",payment_method:e.payment_method??"",reason:Oe(t)||"kcp-window-closed"};try{await te(e.closeReportUrl,n,!0)}catch(o){console.warn("[sirsoft-pay_nhnkcp] failed to report payment window close",o)}}const De="nhnkcp_applepay";function S(e){return e===De}function x(){if(typeof navigator>"u")return!1;const e=navigator,t=(e.userAgent||"").toLowerCase(),n=((e.userAgentData?.platform??e.platform)||"").toLowerCase(),o=e.maxTouchPoints??0;return/iphone|ipad|ipod/.test(t)||/iphone|ipad|ipod|ios/.test(n)?!0:/macintosh|mac os x/.test(t)&&o>1}function M(){return((typeof document<"u"?document.documentElement.lang:"")||"").toLowerCase().startsWith("ko")?"애플페이는 IOS 기기에 모바일결제만 가능합니다.":"Apple Pay is available only for mobile payments on iOS devices."}function T(e){return(e??"KRW").trim().toUpperCase()||"KRW"}function He(e){return T(e)==="KRW"}function Fe(e){const t=T(e);return((typeof document<"u"?document.documentElement.lang:"")||"").toLowerCase().startsWith("ko")?`NHN KCP는 KRW 결제만 지원합니다. (${t})`:`NHN KCP supports KRW payments only. (${t})`}function qe(e){const t=Math.max(0,Math.round(Number(e.amount??0))),n=Math.min(Math.max(0,Math.round(Number(e.tax_free_amount??0))),t);if(n<=0)return{};const o=Math.max(0,Math.round(Number(e.tax_amount??0))),r=Math.max(0,Math.round(Number(e.vat_amount??0))),a=Math.max(0,t-n),s=a>0&&o>0&&r>0?Math.min(a,Math.round(a*(r/o))):0,i=a-s;return{tax_flag:"TG03",comm_tax_mny:String(i),comm_vat_mny:String(s),comm_free_mny:String(n)}}class R extends Error{constructor(t){super(t??"결제가 취소되었습니다."),this.name="KcpCancelledError"}}const Ue={card:"100000000000",bank_transfer:"010000000000",virtual_account:"001000000000",mobile:"000010000000",bank:"010000000000",vbank:"001000000000",phone:"000010000000"},ne={nhnkcp_payco:{payco_direct:"Y"},nhnkcp_naverpay:{naverpay_direct:"Y"},nhnkcp_naverpay_point:{naverpay_direct:"Y",naverpay_point_direct:"Y"},nhnkcp_kakaopay:{kakaopay_direct:"Y"},nhnkcp_applepay:{applepay_direct:"Y"}};function Ye(e,t){const n=e.toLowerCase();return!t||!Object.prototype.hasOwnProperty.call(ne,n)?{}:{param_opt_1:n,nhnkcp_easy_pay_method:n}}function Ge(e){if(!e.isConnected)return!0;const t=window.getComputedStyle(e);if(t.display==="none"||t.visibility==="hidden")return!0;const n=e.getBoundingClientRect();return n.width<=0||n.height<=0}function We(e,t){let n=!1,o,r;const a=()=>{n=!0,r&&(r.disconnect(),r=void 0),o!==void 0&&(window.clearInterval(o),o=void 0)},s=()=>{n||!Ge(e)||(a(),t())},i=document.body??document.documentElement;return i&&typeof MutationObserver<"u"&&(r=new MutationObserver(s),r.observe(i,{attributes:!0,attributeFilter:["class","hidden","style"],childList:!0,subtree:!0})),o=window.setInterval(s,300),a}function ze(){if(typeof navigator>"u")return!1;if(x())return!0;const e=navigator;if(e.userAgentData?.mobile!==void 0)return e.userAgentData.mobile;const t=(navigator.userAgent||"").toLowerCase();return!!(/android|iphone|ipod|windows phone|iemobile|blackberry|opera mini|mobile safari/.test(t)||(navigator.maxTouchPoints??0)>1&&!t.includes("windows")&&!t.includes("macintosh"))}async function Je(e,t){const{pgPaymentData:n,paymentMethod:o}=e.params||{};if(!n){console.error("[sirsoft-pay_nhnkcp] pgPaymentData is required");return}const r=o??n.payment_method??n.pay_method??"card",a=typeof r=="string"&&r.startsWith("nhnkcp_"),s=window.G7Core;let i=null;try{if(!He(n.currency)){const u=Fe(n.currency);s?.state?.setLocal?.({paymentErrorMessage:u,isSubmittingOrder:!1,paymentMethod:r}),s?.modal?.open?.("nhnkcp_payment_error_modal");return}if(S(r)&&!x()){const u=M();s?.state?.setLocal?.({paymentErrorMessage:u,isSubmittingOrder:!1,paymentMethod:r}),s?.modal?.open?.("nhnkcp_payment_error_modal");return}const c=await s.api.get("/modules/sirsoft-ecommerce/payments/client-config/nhnkcp");if(!c.data){console.error("[Line truncated
|
||||
<script src="${e.sdk_url}"><\/script>
|
||||
</head><body style="margin:0;padding:0;background:transparent;">
|
||||
<form name="order_info">${c}</form>
|
||||
@@ -14,4 +14,5 @@ try {
|
||||
window.__kcpFail && window.__kcpFail(e);
|
||||
}
|
||||
<\/script>
|
||||
</body></html>`),Q.close(),y=window.setTimeout(()=>{N(new Error("KCP SDK load timeout"))},15e3),f.__kcpReady=()=>{y!==void 0&&(clearTimeout(y),y=void 0)}})}const Xe={nhnkcp_payco:"PAYCO",nhnkcp_naverpay:"네이버페이",nhnkcp_naverpay_point:"네이버페이 포인트",nhnkcp_kakaopay:"카카오페이",nhnkcp_applepay:"Apple Pay"},Ze="0 0 0 2px #ffffff, 0 0 0 5px rgba(0,0,0,0.55)";function ne(){return document.querySelector(".nhnkcp-easy-pay-btns")??null}function oe(){const e=ne();e&&e.querySelectorAll("button").forEach(t=>{t.style.boxShadow="",t.style.outline=""})}function et(e){const t=ne();if(!t)return;const n=Xe[e];t.querySelectorAll("button").forEach(o=>{o.textContent?.trim()===n?(o.style.boxShadow=Ze,o.style.outline="none"):(o.style.boxShadow="",o.style.outline="")})}function tt(e){const t=e.params?.paymentMethod;if(!t)return;const n=window.G7Core;if(S(t)&&!x()){const r=M();n?.state?.setLocal?.({paymentErrorMessage:r,isSubmittingOrder:!1}),n?.modal?.open?.("nhnkcp_payment_error_modal"),oe();return}const o=typeof t=="string"&&t.startsWith("nhnkcp_");n?.state?.setLocal?.({paymentMethod:t}),o?et(t):oe()}function nt(e){const t=document.createElement("textarea");t.value=e,t.setAttribute("readonly","readonly"),t.style.position="fixed",t.style.left="-9999px",t.style.top="0",document.body.appendChild(t),t.select();try{document.execCommand("copy")}finally{t.remove()}}async function ot(e){const t=String(e.params?.text??"");if(t!==""){if(navigator.clipboard&&typeof navigator.clipboard.writeText=="function"){await navigator.clipboard.writeText(t);return}nt(t)}}const re={requestPayment:Je,setPaymentMethod:tt,copyToClipboard:ot},ae="/api/modules/sirsoft-ecommerce/user/orders",$="sirsoft-pay_nhnkcp",se={info:(...e)=>console.info(`[${$}]`,...e),warn:(...e)=>console.warn(`[${$}]`,...e),error:(...e)=>console.error(`[${$}]`,...e)};function rt(e){return typeof e=="string"?e:e instanceof URL?e.toString():typeof Request<"u"&&e instanceof Request?e.url:String(e)}function at(e,t){return t?.method?t.method.toUpperCase():typeof Request<"u"&&e instanceof Request?e.method.toUpperCase():"GET"}function st(e,t){if(t!=="POST")return!1;const n=e.split("?")[0].split("#")[0];return n===ae||n.endsWith(ae)}function it(e){try{return JSON.parse(e).payment_method}catch{return}}function ct(e){return new Response(JSON.stringify({success:!1,message:e,error:e,errors:{payment_method:[e]}}),{status:422,statusText:"Unprocessable Content",headers:{"Content-Type":"application/json"}})}function dt(){if(typeof window>"u"||typeof window.fetch!="function")return;const e="__sirsoftNhnkcpInterceptorInstalled",t=window;if(t[e])return;t[e]=!0;const n=window.fetch.bind(window);window.fetch=async function(r,a){const s=rt(r),i=at(r,a);if(!st(s,i))return n(r,a);let c;if(a?.body&&typeof a.body=="string"&&(c=it(a.body)),S(c)&&!x()){const d=M();return se.warn(d),ct(d)}return n(r,a)},se.info("order request interceptor installed (Apple Pay device guard)")}const L="sirsoft-pay_nhnkcp",ie="__kcpOcReceiptInjectorInstalled",ce="kcp-oc-receipt-btn",lt=/^\/shop\/orders\/([^/]+)\/complete$/;function de(){return localStorage.getItem("auth_token")}async function ut(e){const t=de();if(!t)return null;try{const n=await fetch(`/api/modules/sirsoft-ecommerce/user/orders/${e}`,{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}});return n.ok?(await n.json())?.data?.payment??null:null}catch{return null}}async function O(e){const t=de();if(!t)return null;try{const n=await fetch(`/api/plugins/${L}/user/orders/${e}/receipt`,{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}});return n.ok?await n.json():null}catch{return null}}function le(e,t,n){const o=document.createElement("button");return o.type="button",o.className=t,o.textContent=e,o.addEventListener("click",()=>n(o)),o}function pt(e){if(!e)return!1;const t=Array.from(document.querySelectorAll("div"));for(const n of t){const o=Array.from(n.children).filter(s=>s instanceof HTMLElement&&s.tagName==="SPAN");if(o.length<2)continue;const r=o[0].textContent?.trim();if(r!=="결제 방법"&&r!=="결제수단"&&r!=="결제 방식")continue;const a=o[o.length-1];return a.textContent?.trim()===e?(n.dataset.nhnkcpPaymentMethodRow="true",!0):(a.textContent=e,a.dataset.nhnkcpPaymentMethodPatched="true",n.dataset.nhnkcpPaymentMethodRow="true",!0)}return!1}async function mt(e){if(document.getElementById(ce))return;const t=await ut(e);if(!t||t.pg_provider!=="nhnkcp"||!t.transaction_id||!t.paid_at)return;const n=Array.from(document.querySelectorAll('button[type="button"]')).find(c=>c.className.includes("bg-blue-600"));if(!n?.parentElement)return;const o=await O(e);pt(o?.payment_method_display_label);const r=n.parentElement,a=le("영수증 조회",n.className.replace(/bg-blue-\d+/g,"bg-green-600").replace(/hover:bg-blue-\d+/g,"hover:bg-green-700"),async c=>{c.disabled=!0,c.textContent="로딩 중...";const d=await O(e);if(c.disabled=!1,c.textContent="영수증 조회",d){const l=window.KcpReceiptPopup;l&&new l({url:d.reLine truncated
|
||||
</body></html>`),X.close(),y=window.setTimeout(()=>{N(new Error("KCP SDK load timeout"))},15e3),f.__kcpReady=()=>{y!==void 0&&(clearTimeout(y),y=void 0)}})}const Xe={nhnkcp_payco:"PAYCO",nhnkcp_naverpay:"네이버페이",nhnkcp_naverpay_point:"네이버페이 포인트",nhnkcp_kakaopay:"카카오페이",nhnkcp_applepay:"Apple Pay"},Ze="0 0 0 2px #ffffff, 0 0 0 5px rgba(0,0,0,0.55)";function oe(){return document.querySelector(".nhnkcp-easy-pay-btns")??null}function re(){const e=oe();e&&e.querySelectorAll("button").forEach(t=>{t.style.boxShadow="",t.style.outline=""})}function et(e){const t=oe();if(!t)return;const n=Xe[e];t.querySelectorAll("button").forEach(o=>{o.textContent?.trim()===n?(o.style.boxShadow=Ze,o.style.outline="none"):(o.style.boxShadow="",o.style.outline="")})}function tt(e){const t=e.params?.paymentMethod;if(!t)return;const n=window.G7Core;if(S(t)&&!x()){const r=M();n?.state?.setLocal?.({paymentErrorMessage:r,isSubmittingOrder:!1}),n?.modal?.open?.("nhnkcp_payment_error_modal"),re();return}const o=typeof t=="string"&&t.startsWith("nhnkcp_");n?.state?.setLocal?.({paymentMethod:t}),o?et(t):re()}function nt(e){const t=document.createElement("textarea");t.value=e,t.setAttribute("readonly","readonly"),t.style.position="fixed",t.style.left="-9999px",t.style.top="0",document.body.appendChild(t),t.select();try{document.execCommand("copy")}finally{t.remove()}}async function ot(e){const t=String(e.params?.text??"");if(t!==""){if(navigator.clipboard&&typeof navigator.clipboard.writeText=="function"){await navigator.clipboard.writeText(t);return}nt(t)}}const ae={requestPayment:Je,setPaymentMethod:tt,copyToClipboard:ot},se="/api/modules/sirsoft-ecommerce/user/orders",$="sirsoft-pay_nhnkcp",ie={info:(...e)=>console.info(`[${$}]`,...e),warn:(...e)=>console.warn(`[${$}]`,...e),error:(...e)=>console.error(`[${$}]`,...e)};function rt(e){return typeof e=="string"?e:e instanceof URL?e.toString():typeof Request<"u"&&e instanceof Request?e.url:String(e)}function at(e,t){return t?.method?t.method.toUpperCase():typeof Request<"u"&&e instanceof Request?e.method.toUpperCase():"GET"}function st(e,t){if(t!=="POST")return!1;const n=e.split("?")[0].split("#")[0];return n===se||n.endsWith(se)}function it(e){try{return JSON.parse(e).payment_method}catch{return}}function ct(e){return new Response(JSON.stringify({success:!1,message:e,error:e,errors:{payment_method:[e]}}),{status:422,statusText:"Unprocessable Content",headers:{"Content-Type":"application/json"}})}function dt(){if(typeof window>"u"||typeof window.fetch!="function")return;const e="__sirsoftNhnkcpInterceptorInstalled",t=window;if(t[e])return;t[e]=!0;const n=window.fetch.bind(window);window.fetch=async function(r,a){const s=rt(r),i=at(r,a);if(!st(s,i))return n(r,a);let c;if(a?.body&&typeof a.body=="string"&&(c=it(a.body)),S(c)&&!x()){const d=M();return ie.warn(d),ct(d)}return n(r,a)},ie.info("order request interceptor installed (Apple Pay device guard)")}const L="sirsoft-pay_nhnkcp",ce="__kcpOcReceiptInjectorInstalled",de="kcp-oc-receipt-btn",lt=/^\/shop\/orders\/([^/]+)\/complete$/;function le(){return localStorage.getItem("auth_token")}async function ut(e){const t=le();if(!t)return null;try{const n=await fetch(`/api/modules/sirsoft-ecommerce/user/orders/${e}`,{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}});return n.ok?(await n.json())?.data?.payment??null:null}catch{return null}}async function O(e){const t=le();if(!t)return null;try{const n=await fetch(`/api/plugins/${L}/user/orders/${e}/receipt`,{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}});return n.ok?await n.json():null}catch{return null}}function ue(e,t,n){const o=document.createElement("button");return o.type="button",o.className=t,o.textContent=e,o.addEventListener("click",()=>n(o)),o}function pt(e){if(!e)return!1;const t=Array.from(document.querySelectorAll("div"));for(const n of t){const o=Array.from(n.children).filter(s=>s instanceof HTMLElement&&s.tagName==="SPAN");if(o.length<2)continue;const r=o[0].textContent?.trim();if(r!=="결제 방법"&&r!=="결제수단"&&r!=="결제 방식")continue;const a=o[o.length-1];return a.textContent?.trim()===e?(n.dataset.nhnkcpPaymentMethodRow="true",!0):(a.textContent=e,a.dataset.nhnkcpPaymentMethodPatched="true",n.dataset.nhnkcpPaymentMethodRow="true",!0)}return!1}async function mt(e){if(document.getElementById(de))return;const t=await ut(e);if(!t||t.pg_provider!=="nhnkcp"||!t.transaction_id||!t.paid_at)return;const n=Array.from(document.querySelectorAll('button[type="button"]')).find(c=>c.className.includes("bg-blue-600"));if(!n?.parentElement)return;const o=await O(e);pt(o?.payment_method_display_label);const r=n.parentElement,a=ue("영수증 조회",n.className.replace(/bg-blue-\d+/g,"bg-green-600").replace(/hover:bg-blue-\d+/g,"hover:bg-green-700"),async c=>{c.disabled=!0,c.textContent="로딩 중...";const d=await O(e);if(c.disabled=!1,c.textContent="영수증 조회",d){const l=window.KcpReceiptPopup;l&&new l({url:d.reLine truncated
|
||||
//# sourceMappingURL=plugin.iife.js.map
|
||||
+29
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
installAdminPaymentMethodBrandInjector,
|
||||
resetAdminPaymentMethodBrandInjectorForTests,
|
||||
syncRenderedAdminPaymentMethodBrands,
|
||||
} from '../adminPaymentMethodBrandInjector';
|
||||
@@ -84,4 +85,32 @@ describe('adminPaymentMethodBrandInjector', () => {
|
||||
expect(syncRenderedAdminPaymentMethodBrands()).toBe(false);
|
||||
expect(document.querySelector('[data-nhnkcp-admin-payment-brand-mark="true"]')).toBeNull();
|
||||
});
|
||||
|
||||
/**
|
||||
* 탭 전환 후 복귀 시 배지 재주입 (브라우저 실측으로 발견한 회귀).
|
||||
*
|
||||
* 환경설정 화면의 탭 전환은 pushState 가 아니라 **replaceState** 로 URL 만 바꾼다
|
||||
* (탭 왕복 1회에 push 0 / replace 2 실측). pushState/popstate 만 후킹하던 기존 구현은
|
||||
* 다른 탭에 갔다 돌아왔을 때 onRouteChange 가 발화하지 않아, 행은 다시 렌더되는데
|
||||
* 배지만 사라진 채로 남았다 — 사용자에게는 아이콘이 회색으로 되돌아간 것으로 보인다.
|
||||
*/
|
||||
it('replaceState 로 탭을 전환했다 돌아와도 배지를 재주입한다', async () => {
|
||||
installAdminPaymentMethodBrandInjector();
|
||||
expect(document.querySelectorAll('[data-nhnkcp-admin-payment-brand-mark="true"]').length).toBeGreaterThan(0);
|
||||
|
||||
// 다른 탭으로 이동 (replaceState) — 화면이 갈아끼워지며 배지가 사라진 상태를 재현
|
||||
window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=shipping');
|
||||
document.body.innerHTML = '';
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
|
||||
// 주문설정으로 복귀 (replaceState) + 행 재렌더
|
||||
document.body.innerHTML = desktopItem('payco', 'PAYCO', 'PAYCO로 결제 (NHN KCP)');
|
||||
window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=order_settings');
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
|
||||
expect(
|
||||
document.querySelectorAll('[data-nhnkcp-admin-payment-brand-mark="true"]').length,
|
||||
'replaceState 복귀 후 배지가 재주입되지 않았다',
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -263,6 +263,17 @@ export function installAdminPaymentMethodBrandInjector(): void {
|
||||
origPush(...args);
|
||||
window.setTimeout(() => onRouteChange(), 200);
|
||||
};
|
||||
|
||||
// replaceState 도 후킹한다 — 환경설정 화면의 탭 전환은 pushState 가 아니라
|
||||
// replaceState 로 URL 만 바꾼다(실측: 탭 왕복 1회에 push 0 / replace 2). pushState 만
|
||||
// 후킹하면 다른 탭에 갔다 돌아왔을 때 onRouteChange 가 발화하지 않아 배지가 사라진 채로
|
||||
// 남는다(행은 그대로 렌더되므로 화면상 아이콘만 회색으로 되돌아간다).
|
||||
const origReplace = history.replaceState.bind(history);
|
||||
history.replaceState = (...args: Parameters<typeof history.replaceState>) => {
|
||||
origReplace(...args);
|
||||
window.setTimeout(() => onRouteChange(), 200);
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', () => window.setTimeout(() => onRouteChange(), 200));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.1] - 2026-07-22
|
||||
## [1.0.1] - 2026-08-10
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- 이커머스 관리자 주문설정에서 다른 탭에 갔다 돌아오면 간편결제 항목의 브랜드 색상 배지가 사라지고 회색 기본 아이콘으로 남던 문제를 수정했습니다. 화면을 새로고침해야만 다시 표시됐습니다.
|
||||
- 결제 취소 기록과 환불 금액 오류 안내에 원화 단위가 고정 표기되던 문제를 수정했습니다. 이제 실제 결제 통화로 표시됩니다.
|
||||
- 쇼핑몰 주소를 기본값이 아닌 값으로 바꾸거나 주소 없이 운영하는 상점에서, 결제를 마치거나 결제에 실패한 구매자가 존재하지 않는 페이지로 이동하던 문제를 수정했습니다. 이동 주소가 기본 주소로 고정돼 있었기 때문이며, 이제 상점 주소 설정을 따라갑니다. 이미 운영 중인 사이트도 업데이트 시 자동으로 반영되며, 이동 주소를 직접 입력해 두었다면 그 값은 그대로 유지됩니다. (#85 @hwaryeon1234 님께서 제보해주셨습니다.)
|
||||
- 이 플러그인이 설치된 사이트에서 관리자 주문 목록의 '일부취소' 표시와 적립예정 포인트·마일리지 사용 금액, 주문상품의 추가옵션 요약이 보이지 않던 문제를 수정했습니다. 결제 정보 칸에 테스트결제 표시를 덧붙이는 과정에서 목록 표 구성 전체를 옛 버전으로 되돌려 놓고 있었습니다.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"identifier": "sirsoft-pay_nicepayments",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"components": {
|
||||
"basic": [],
|
||||
"composite": [],
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
(function(){"use strict";function le(e){return/^https?:\/\//i.test(e)||e.startsWith("/api/")?e:e.startsWith("/plugins/")?`/api${e}`:e.startsWith("plugins/")?`/api/${e}`:e}function de(e){return e.trim().slice(0,160)}async function ue(e,t="nicepay-window-closed"){if(!e.closeReportUrl)return;const n={oid:e.oid,price:Number(e.price),buyer_email:e.buyer_email??"",buyer_phone:e.buyer_phone??"",payment_method:e.payment_method??"",reason:de(t)||"nicepay-window-closed"};try{const a=window.G7Core?.api;if(typeof a?.post=="function"){await a.post(e.closeReportUrl,n);return}await fetch(le(e.closeReportUrl),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n),keepalive:!0})}catch(a){console.warn("[sirsoft-pay_nicepayments] failed to report payment window close",a)}}function pe(e){return new Promise((t,n)=>{if(document.querySelector(`script[src="${e}"]`)){t();return}const a=document.createElement("script");a.src=e,a.onload=()=>t(),a.onerror=()=>n(new Error(`Failed to load script: ${e}`)),document.head.appendChild(a)})}function me(e,t){const n=document.createElement("form");n.id="nicepayForm",n.method="post",n.action=e,n.acceptCharset="utf-8";for(const[a,r]of Object.entries(t)){const o=document.createElement("input");o.type="hidden",o.name=a,o.value=r,n.appendChild(o)}return document.body.appendChild(n),n}const ye="/plugins/sirsoft-pay_nicepayments/payment/callback",fe="https://web.nicepay.co.kr/v3/v3Payment.jsp";function he(){if(typeof navigator>"u")return!1;const e=navigator;if(e.userAgentData?.mobile!==void 0)return e.userAgentData.mobile;const t=(navigator.userAgent||"").toLowerCase();return!!(/android|iphone|ipod|windows phone|iemobile|blackberry|opera mini|mobile safari/.test(t)||(navigator.maxTouchPoints??0)>1&&!t.includes("windows")&&!t.includes("macintosh"))}async function be(e,t){const{pgPaymentData:n,paymentMethod:a}=e.params??{},r=window.__templateApp?.globalState?._local,o=a??n?.payment_method??r?.paymentMethod??"card";if(!n){console.error("[sirsoft-pay_nicepayments] pgPaymentData is required");return}const i=window.G7Core;try{const s=await i.api.get("/modules/sirsoft-ecommerce/payments/client-config/nicepayments");if(!s.data){console.error("[sirsoft-pay_nicepayments] Failed to fetch client config",s);return}const l=s.data,d=he();if(!d&&(await pe(l.sdk_url),typeof window.goPay!="function")){i?.toast?.error?.("나이스페이먼츠 SDK를 불러올 수 없습니다. 잠시 후 다시 시도해주세요."),i?.state?.setLocal?.({isSubmittingOrder:!1,paymentMethod:o});return}const w=window.location.origin+l.sign_data_url,u=typeof localStorage<"u"?localStorage.getItem("auth_token"):null,te={"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest",Accept:"application/json"};u&&(te.Authorization=`Bearer ${u}`);const ne=await fetch(w,{method:"POST",credentials:"include",headers:te,body:JSON.stringify({amt:n.amount,moid:n.order_number,buyer_email:n.customer_email??"",buyer_phone:n.customer_phone??""})});if(!ne.ok)throw new Error("SignData 생성에 실패했습니다.");const I=await ne.json(),ae=window.location.origin+ye,L=typeof o=="string"&&o.startsWith("nicepay_"),oe=L?"CARD":{card:"CARD",vbank:"VBANK",bank:"BANK",phone:"CELLPHONE"}[o??"card"]??"CARD",c={PayMethod:oe,GoodsName:n.order_name,Amt:String(n.amount),MID:I.mid,Moid:n.order_number,BuyerName:n.customer_name??"",BuyerEmail:n.customer_email??"",BuyerTel:n.customer_phone??"",ReturnURL:ae,EdiDate:I.ediDate,SignData:I.signData,CharSet:"utf-8",GoodsCl:"1",DirectShowOpt:"",DirectEasyPay:"",NicepayReserved:"",EasyPayMethod:"",MallReserved:"",MallReserved1:""};oe==="CELLPHONE"&&(c.GoodsCl=n.goods_cl??"1");const ft=l.useEscrow&&!L;if(c.TransType=ft?"1":"0",L)switch(c.DirectShowOpt="CARD",c.MallReserved=`nicepay_easy_pay_method=${encodeURIComponent(o)}`,c.MallReserved1=o,o){case"nicepay_naverpay":c.DirectEasyPay="E020",c.EasyPayMethod="E020=CARD";break;case"nicepay_kakaopay":c.NicepayReserved="DirectKakao=Y";break;case"nicepay_samsungpay":c.DirectEasyPay="E021";break;case"nicepay_applepay":c.DirectEasyPay="E025";break;case"nicepay_payco":c.NicepayReserved="DirectPayco=Y";break;case"nicepay_skpay":c.NicepayReserved="DirectPay11=Y";break;case"nicepay_ssgpay":c.DirectEasyPay="E007";break;case"nicepay_lpay":c.DirectEasyPay="E018";break}try{const m=(await i.api.get(`/modules/sirsoft-ecommerce/user/orders/${n.order_number}`))?.data;if(m){const P=Number(m.total_tax_amount??0),h=Number(m.total_vat_amount??0),ce=Number(m.total_tax_free_amount??0);(P>0||h>0||ce>0)&&(c.TaxAmt=String(P),c.VatAmt=String(h),c.TaxFreeAmt=String(ce))}}catch{}const p=me(ae,c);if(d){p.action=fe,p.acceptCharset="euc-kr";const f=p.querySelector('input[name="CharSet"]');f&&(f.value="euc-kr"),p.submit();return}window.nicepaySubmit=()=>{p.submit()};let re=!1;const ht=new Set(document.body.children),ie=(f,m)=>{if(re)return;re=!0;const P=[f,m].filter(Boolean).join(": ")||"nicepay-window-closed";ue({closeReportUrl:l.close_report_url,oid:n.order_number,price:Number(n.aLine truncated
|
||||
(function(){"use strict";function le(e){return/^https?:\/\//i.test(e)||e.startsWith("/api/")?e:e.startsWith("/plugins/")?`/api${e}`:e.startsWith("plugins/")?`/api/${e}`:e}function de(e){return e.trim().slice(0,160)}async function ue(e,t="nicepay-window-closed"){if(!e.closeReportUrl)return;const n={oid:e.oid,price:Number(e.price),buyer_email:e.buyer_email??"",buyer_phone:e.buyer_phone??"",payment_method:e.payment_method??"",reason:de(t)||"nicepay-window-closed"};try{const a=window.G7Core?.api;if(typeof a?.post=="function"){await a.post(e.closeReportUrl,n);return}await fetch(le(e.closeReportUrl),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n),keepalive:!0})}catch(a){console.warn("[sirsoft-pay_nicepayments] failed to report payment window close",a)}}function pe(e){return new Promise((t,n)=>{if(document.querySelector(`script[src="${e}"]`)){t();return}const a=document.createElement("script");a.src=e,a.onload=()=>t(),a.onerror=()=>n(new Error(`Failed to load script: ${e}`)),document.head.appendChild(a)})}function me(e,t){const n=document.createElement("form");n.id="nicepayForm",n.method="post",n.action=e,n.acceptCharset="utf-8";for(const[a,r]of Object.entries(t)){const o=document.createElement("input");o.type="hidden",o.name=a,o.value=r,n.appendChild(o)}return document.body.appendChild(n),n}const ye="/plugins/sirsoft-pay_nicepayments/payment/callback",fe="https://web.nicepay.co.kr/v3/v3Payment.jsp";function he(){if(typeof navigator>"u")return!1;const e=navigator;if(e.userAgentData?.mobile!==void 0)return e.userAgentData.mobile;const t=(navigator.userAgent||"").toLowerCase();return!!(/android|iphone|ipod|windows phone|iemobile|blackberry|opera mini|mobile safari/.test(t)||(navigator.maxTouchPoints??0)>1&&!t.includes("windows")&&!t.includes("macintosh"))}async function be(e,t){const{pgPaymentData:n,paymentMethod:a}=e.params??{},r=window.__templateApp?.globalState?._local,o=a??n?.payment_method??r?.paymentMethod??"card";if(!n){console.error("[sirsoft-pay_nicepayments] pgPaymentData is required");return}const i=window.G7Core;try{const s=await i.api.get("/modules/sirsoft-ecommerce/payments/client-config/nicepayments");if(!s.data){console.error("[sirsoft-pay_nicepayments] Failed to fetch client config",s);return}const l=s.data,d=he();if(!d&&(await pe(l.sdk_url),typeof window.goPay!="function")){i?.toast?.error?.("나이스페이먼츠 SDK를 불러올 수 없습니다. 잠시 후 다시 시도해주세요."),i?.state?.setLocal?.({isSubmittingOrder:!1,paymentMethod:o});return}const _=window.location.origin+l.sign_data_url,u=typeof localStorage<"u"?localStorage.getItem("auth_token"):null,te={"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest",Accept:"application/json"};u&&(te.Authorization=`Bearer ${u}`);const ne=await fetch(_,{method:"POST",credentials:"include",headers:te,body:JSON.stringify({amt:n.amount,moid:n.order_number,buyer_email:n.customer_email??"",buyer_phone:n.customer_phone??""})});if(!ne.ok)throw new Error("SignData 생성에 실패했습니다.");const L=await ne.json(),ae=window.location.origin+ye,M=typeof o=="string"&&o.startsWith("nicepay_"),oe=M?"CARD":{card:"CARD",vbank:"VBANK",bank:"BANK",phone:"CELLPHONE"}[o??"card"]??"CARD",c={PayMethod:oe,GoodsName:n.order_name,Amt:String(n.amount),MID:L.mid,Moid:n.order_number,BuyerName:n.customer_name??"",BuyerEmail:n.customer_email??"",BuyerTel:n.customer_phone??"",ReturnURL:ae,EdiDate:L.ediDate,SignData:L.signData,CharSet:"utf-8",GoodsCl:"1",DirectShowOpt:"",DirectEasyPay:"",NicepayReserved:"",EasyPayMethod:"",MallReserved:"",MallReserved1:""};oe==="CELLPHONE"&&(c.GoodsCl=n.goods_cl??"1");const ft=l.useEscrow&&!M;if(c.TransType=ft?"1":"0",M)switch(c.DirectShowOpt="CARD",c.MallReserved=`nicepay_easy_pay_method=${encodeURIComponent(o)}`,c.MallReserved1=o,o){case"nicepay_naverpay":c.DirectEasyPay="E020",c.EasyPayMethod="E020=CARD";break;case"nicepay_kakaopay":c.NicepayReserved="DirectKakao=Y";break;case"nicepay_samsungpay":c.DirectEasyPay="E021";break;case"nicepay_applepay":c.DirectEasyPay="E025";break;case"nicepay_payco":c.NicepayReserved="DirectPayco=Y";break;case"nicepay_skpay":c.NicepayReserved="DirectPay11=Y";break;case"nicepay_ssgpay":c.DirectEasyPay="E007";break;case"nicepay_lpay":c.DirectEasyPay="E018";break}try{const m=(await i.api.get(`/modules/sirsoft-ecommerce/user/orders/${n.order_number}`))?.data;if(m){const P=Number(m.total_tax_amount??0),h=Number(m.total_vat_amount??0),ce=Number(m.total_tax_free_amount??0);(P>0||h>0||ce>0)&&(c.TaxAmt=String(P),c.VatAmt=String(h),c.TaxFreeAmt=String(ce))}}catch{}const p=me(ae,c);if(d){p.action=fe,p.acceptCharset="euc-kr";const f=p.querySelector('input[name="CharSet"]');f&&(f.value="euc-kr"),p.submit();return}window.nicepaySubmit=()=>{p.submit()};let re=!1;const ht=new Set(document.body.children),ie=(f,m)=>{if(re)return;re=!0;const P=[f,m].filter(Boolean).join(": ")||"nicepay-window-closed";ue({closeReportUrl:l.close_report_url,oid:n.order_number,price:Number(n.aLine truncated
|
||||
//# sourceMappingURL=plugin.iife.js.map
|
||||
+29
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
installAdminPaymentMethodBrandInjector,
|
||||
resetAdminPaymentMethodBrandInjectorForTests,
|
||||
syncRenderedAdminPaymentMethodBrands,
|
||||
} from '../adminPaymentMethodBrandInjector';
|
||||
@@ -84,4 +85,32 @@ describe('adminPaymentMethodBrandInjector', () => {
|
||||
expect(syncRenderedAdminPaymentMethodBrands()).toBe(false);
|
||||
expect(document.querySelector('[data-nicepay-admin-payment-brand-mark="true"]')).toBeNull();
|
||||
});
|
||||
|
||||
/**
|
||||
* 탭 전환 후 복귀 시 배지 재주입 (브라우저 실측으로 발견한 회귀).
|
||||
*
|
||||
* 환경설정 화면의 탭 전환은 pushState 가 아니라 **replaceState** 로 URL 만 바꾼다
|
||||
* (탭 왕복 1회에 push 0 / replace 2 실측). pushState/popstate 만 후킹하던 기존 구현은
|
||||
* 다른 탭에 갔다 돌아왔을 때 onRouteChange 가 발화하지 않아, 행은 다시 렌더되는데
|
||||
* 배지만 사라진 채로 남았다 — 사용자에게는 아이콘이 회색으로 되돌아간 것으로 보인다.
|
||||
*/
|
||||
it('replaceState 로 탭을 전환했다 돌아와도 배지를 재주입한다', async () => {
|
||||
installAdminPaymentMethodBrandInjector();
|
||||
expect(document.querySelectorAll('[data-nicepay-admin-payment-brand-mark="true"]').length).toBeGreaterThan(0);
|
||||
|
||||
// 다른 탭으로 이동 (replaceState) — 화면이 갈아끼워지며 배지가 사라진 상태를 재현
|
||||
window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=shipping');
|
||||
document.body.innerHTML = '';
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
|
||||
// 주문설정으로 복귀 (replaceState) + 행 재렌더
|
||||
document.body.innerHTML = desktopItem('naverpay', '네이버페이 (나이스페이먼츠)', '네이버페이로 결제 (나이스페이먼츠)');
|
||||
window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=order_settings');
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
|
||||
expect(
|
||||
document.querySelectorAll('[data-nicepay-admin-payment-brand-mark="true"]').length,
|
||||
'replaceState 복귀 후 배지가 재주입되지 않았다',
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
+11
@@ -272,6 +272,17 @@ export function installAdminPaymentMethodBrandInjector(): void {
|
||||
origPush(...args);
|
||||
window.setTimeout(() => onRouteChange(), 200);
|
||||
};
|
||||
|
||||
// replaceState 도 후킹한다 — 환경설정 화면의 탭 전환은 pushState 가 아니라
|
||||
// replaceState 로 URL 만 바꾼다(실측: 탭 왕복 1회에 push 0 / replace 2). pushState 만
|
||||
// 후킹하면 다른 탭에 갔다 돌아왔을 때 onRouteChange 가 발화하지 않아 배지가 사라진 채로
|
||||
// 남는다(행은 그대로 렌더되므로 화면상 아이콘만 회색으로 되돌아간다).
|
||||
const origReplace = history.replaceState.bind(history);
|
||||
history.replaceState = (...args: Parameters<typeof history.replaceState>) => {
|
||||
origReplace(...args);
|
||||
window.setTimeout(() => onRouteChange(), 200);
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', () => window.setTimeout(() => onRouteChange(), 200));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.0] - 2026-07-13
|
||||
## [1.0.0] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
(function(){"use strict";function b(t){return new Promise((r,o)=>{if(document.querySelector(`script[src="${t}"]`)){r();return}const e=document.createElement("script");e.src=t,e.async=!0,e.onload=()=>r(),e.onerror=()=>o(new Error(`Failed to load script: ${t}`)),document.head.appendChild(e)})}function C(t,r){if(!t.order_sheet_mode||!r)return{method:"CARD",easyPay:null};const o=(t.enabled_methods??[]).find(e=>e.id===r);return o?{method:o.method,easyPay:o.easy_pay_provider}:{method:"CARD",easyPay:null}}function p(t){if(t==="on")return!0;if(t==="off")return!1}function E(t,r){const o={validHours:t.vbank?.valid_hours??24},e=t.vbank?.cash_receipt_type??"";e&&(o.cashReceipt={type:e});const s=p(t.use_escrow);return s!==void 0&&(o.useEscrow=s),o}function w(t,r,o){if(r.use_escrow==="off")return;const e=o.escrow_products??[];e.length>0&&(t.escrowProducts=e)}async function A(t,r){const o=t.params||{},{pgPaymentData:e}=o;if(!e){console.error("[sirsoft-tosspayments] pgPaymentData is required");return}const s=window.G7Core,l=o.paymentMethod??s?.state?.getLocal?.()?.paymentMethod;try{const n=await s.api.get("/modules/sirsoft-ecommerce/payments/client-config/tosspayments");if(!n.data){console.error("[sirsoft-tosspayments] Failed to fetch client config",n);return}const a=n.data;if(window.TossPayments||await b(a.sdk_url),window.TossPayments||await new Promise(u=>setTimeout(u,100)),!window.TossPayments){console.error("[sirsoft-tosspayments] TossPayments SDK not available");return}const R=window.TossPayments(a.client_key).payment({customerKey:e.customer_key??window.TossPayments.ANONYMOUS}),f=e.currency??"KRW",{method:d,easyPay:y}=C(a,l);if(f!=="KRW"&&(d!=="CARD"||y!==null)){console.warn("[sirsoft-tosspayments] non-KRW currency supports card only",{currency:f,method:d}),s?.state?.setLocal?.({paymentErrorMessage:s?.t?.("sirsoft-tosspayments.errors.non_krw_method")??"This payment method is available for KRW only.",isSubmittingOrder:!1}),s?.modal?.open?.("tosspayments_payment_error_modal");return}const v=window.location.origin,c={method:d,amount:{currency:f,value:e.amount},orderId:e.order_number,orderName:e.order_name,successUrl:v+a.callback_urls.success,failUrl:v+a.callback_urls.fail,customerEmail:e.customer_email??void 0,customerName:e.customer_name??void 0,customerMobilePhone:e.customer_phone??void 0};if(d==="CARD")c.card={flowMode:"DEFAULT",useCardPoint:!1,useAppCardOnly:!1},y&&(c.easyPay={provider:y});else if(d==="VIRTUAL_ACCOUNT")c.virtualAccount=E(a,e),w(c,a,e);else if(d==="TRANSFER"){const u=p(a.use_escrow);u!==void 0&&(c.transfer={useEscrow:u}),w(c,a,e)}await R.requestPayment(c)}catch(n){if(console.error("[sirsoft-tosspayments] requestPayment error",n),n?.code==="USER_CANCEL"){console.info("[sirsoft-tosspayments] Payment cancelled by user");try{await s.api.post(`/modules/sirsoft-ecommerce/orders/${e.order_number}/cancel-payment`,{cancel_code:n.code,cancel_message:n.message})}catch(P){console.warn("[sirsoft-tosspayments] Failed to record cancellation",P)}s?.state?.setLocal?.({isSubmittingOrder:!1}),s?.modal?.open?.("tosspayments_payment_cancel_modal");return}const a=n?.message??"Unknown error";s?.state?.setLocal?.({paymentErrorMessage:a,isSubmittingOrder:!1}),s?.modal?.open?.("tosspayments_payment_error_modal")}}const _={requestPayment:A},i="sirsoft-tosspayments",m={info:(...t)=>console.info(`[${i}]`,...t),warn:(...t)=>console.warn(`[${i}]`,...t),error:(...t)=>console.error(`[${i}]`,...t)};function h(){const t=window.G7Core;if(!t)return 0;const r=t.getActionDispatcher;if(typeof r!="function")return 0;const o=r();if(!o||typeof o.registerHandler!="function")return 0;let e=0;for(const[s,l]of Object.entries(_)){const n=`${i}.${s}`;o.registerHandler(n,l,{category:"plugin",source:i}),e++}return e}function g(){const t=()=>{const r=h();if(r>0){m.info(`${r} handler(s) registered`);return}let o=0;const e=50,s=setInterval(()=>{o++;const l=h();if(l>0){clearInterval(s),m.info(`${l} handler(s) registered (after ${o} retries)`);return}o>=e&&(clearInterval(s),m.warn("ActionDispatcher not available after timeout"))},100)};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t):t()}g(),window.__SirsoftTosspayments={identifier:i,handlers:Object.keys(_),initPlugin:g}})();
|
||||
(function(){"use strict";function O(e){return new Promise((t,s)=>{if(document.querySelector(`script[src="${e}"]`)){t();return}const n=document.createElement("script");n.src=e,n.async=!0,n.onload=()=>t(),n.onerror=()=>s(new Error(`Failed to load script: ${e}`)),document.head.appendChild(n)})}function $(e,t){if(!e.order_sheet_mode||!t)return{method:"CARD",easyPay:null};const s=(e.enabled_methods??[]).find(n=>n.id===t);return s?{method:s.method,easyPay:s.easy_pay_provider}:{method:"CARD",easyPay:null}}function T(e){if(e==="on")return!0;if(e==="off")return!1}function q(e,t){const s={validHours:e.vbank?.valid_hours??24},n=e.vbank?.cash_receipt_type??"";n&&(s.cashReceipt={type:n});const o=T(e.use_escrow);return o!==void 0&&(s.useEscrow=o),s}function A(e,t,s){if(t.use_escrow==="off")return;const n=s.escrow_products??[];n.length>0&&(e.escrowProducts=n)}async function F(e,t){const s=e.params||{},{pgPaymentData:n}=s;if(!n){console.error("[sirsoft-tosspayments] pgPaymentData is required");return}const o=window.G7Core,r=s.paymentMethod??o?.state?.getLocal?.()?.paymentMethod;try{const a=await o.api.get("/modules/sirsoft-ecommerce/payments/client-config/tosspayments");if(!a.data){console.error("[sirsoft-tosspayments] Failed to fetch client config",a);return}const i=a.data;if(window.TossPayments||await O(i.sdk_url),window.TossPayments||await new Promise(m=>setTimeout(m,100)),!window.TossPayments){console.error("[sirsoft-tosspayments] TossPayments SDK not available");return}const te=window.TossPayments(i.client_key).payment({customerKey:n.customer_key??window.TossPayments.ANONYMOUS}),g=n.currency??"KRW",{method:d,easyPay:b}=$(i,r);if(g!=="KRW"&&(d!=="CARD"||b!==null)){console.warn("[sirsoft-tosspayments] non-KRW currency supports card only",{currency:g,method:d}),o?.state?.setLocal?.({paymentErrorMessage:o?.t?.("sirsoft-tosspayments.errors.non_krw_method")??"This payment method is available for KRW only.",isSubmittingOrder:!1}),o?.modal?.open?.("tosspayments_payment_error_modal");return}const D=window.location.origin,c={method:d,amount:{currency:g,value:n.amount},orderId:n.order_number,orderName:n.order_name,successUrl:D+i.callback_urls.success,failUrl:D+i.callback_urls.fail,customerEmail:n.customer_email??void 0,customerName:n.customer_name??void 0,customerMobilePhone:n.customer_phone??void 0};if(d==="CARD")c.card={flowMode:"DEFAULT",useCardPoint:!1,useAppCardOnly:!1},b&&(c.card.flowMode="DIRECT",c.card.easyPay=b);else if(d==="VIRTUAL_ACCOUNT")c.virtualAccount=q(i,n),A(c,i,n);else if(d==="TRANSFER"){const m=T(i.use_escrow);m!==void 0&&(c.transfer={useEscrow:m}),A(c,i,n)}await te.requestPayment(c)}catch(a){if(console.error("[sirsoft-tosspayments] requestPayment error",a),a?.code==="USER_CANCEL"){console.info("[sirsoft-tosspayments] Payment cancelled by user");try{await o.api.post(`/modules/sirsoft-ecommerce/orders/${n.order_number}/cancel-payment`,{cancel_code:a.code,cancel_message:a.message})}catch(x){console.warn("[sirsoft-tosspayments] Failed to record cancellation",x)}o?.state?.setLocal?.({isSubmittingOrder:!1}),o?.modal?.open?.("tosspayments_payment_cancel_modal");return}const i=a?.message??"Unknown error";o?.state?.setLocal?.({paymentErrorMessage:i,isSubmittingOrder:!1}),o?.modal?.open?.("tosspayments_payment_error_modal")}}const v={requestPayment:F},U="sirsoft-tosspayments",E="__tossAdminPaymentMethodBrandInjectorInstalled",S="__tossAdminPaymentMethodBrandSyncListenerAttached",B=/^\/admin\/ecommerce\/settings\/?$/,L='[data-toss-admin-payment-brand-mark="true"]',K=200,G=120,H=[{id:"toss_tosspay",labels:["토스페이 (토스페이먼츠)","토스페이 간편결제 — 토스페이먼츠를 통해 처리","TossPay (TossPayments)","TossPay easy payment — processed via TossPayments"],shortLabels:[],markLines:["T"],markClassName:"bg-blue-500 text-white"},{id:"toss_kakaopay",labels:["카카오페이 (토스페이먼츠)","카카오페이 간편결제 — 토스페이먼츠를 통해 처리","KakaoPay (TossPayments)","KakaoPay easy payment — processed via TossPayments"],shortLabels:[],markLines:["K"],markClassName:"bg-yellow-400 text-gray-950"},{id:"toss_naverpay",labels:["네이버페이 (토스페이먼츠)","네이버페이 간편결제 — 토스페이먼츠를 통해 처리","NaverPay (TossPayments)","NaverPay easy payment — processed via TossPayments"],shortLabels:[],markLines:["N"],markClassName:"bg-green-500 text-white"},{id:"toss_payco",labels:["페이코 (토스페이먼츠)","페이코 간편결제 — 토스페이먼츠를 통해 처리","PAYCO (TossPayments)","PAYCO easy payment — processed via TossPayments"],shortLabels:[],markLines:["P"],markClassName:"bg-red-500 text-white"},{id:"toss_samsungpay",labels:["삼성페이 (토스페이먼츠)","삼성페이 간편결제 — 토스페이먼츠를 통해 처리","Samsung Pay (TossPayments)","Samsung Pay easy payment — processed via TossPayments"],shortLabels:[],markLines:["S"],markClassName:"bg-blue-600 text-white"}];let M=null,u=null,y=!1;function Y(Line truncated
|
||||
//# sourceMappingURL=plugin.iife.js.map
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 관리자 주문설정 브랜드 마크 주입 회귀.
|
||||
*
|
||||
* 배경: #475 "확장 결제수단 1급화" 가 nicepayments·nhnkcp·kginicis 에 관리자 브랜드
|
||||
* injector 를 도입할 때 토스는 전환 대상에서 빠졌다(그 커밋에 tosspayments 파일 0건).
|
||||
* 관리자 레이아웃은 아이콘 열을 `Icon name={{$method._cached_icon}}` 하나로만 그리므로,
|
||||
* injector 가 없는 플러그인의 결제수단은 브랜드 배지 없이 회색 기본 아이콘으로 남는다.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
installAdminPaymentMethodBrandInjector,
|
||||
resetAdminPaymentMethodBrandInjectorForTests,
|
||||
syncRenderedAdminPaymentMethodBrands,
|
||||
} from '../adminPaymentMethodBrandInjector';
|
||||
|
||||
function desktopItem(testId: string, title: string, description: string): string {
|
||||
return `
|
||||
<div class="flex-center border rounded-lg p-3 gap-4" data-test-item="${testId}">
|
||||
<div data-drag-handle="true"><svg data-drag-icon="true"></svg></div>
|
||||
<svg data-original-icon="true"></svg>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex-center gap-2">
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">${title}</span>
|
||||
</div>
|
||||
<span class="text-label-subtle">${description}</span>
|
||||
</div>
|
||||
<div class="row-stack">
|
||||
<span>PG사</span>
|
||||
<select></select>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function mobileItem(testId: string, title: string): string {
|
||||
return `
|
||||
<div class="excel-card" data-test-item="${testId}">
|
||||
<div class="excel-card-header">
|
||||
<div class="flex-center gap-3 flex-1 min-w-0">
|
||||
<div data-drag-handle="true"><svg data-drag-icon="true"></svg></div>
|
||||
<svg data-original-icon="true"></svg>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100 truncate">${title}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="excel-card-body">
|
||||
<div class="row-stack"><label>PG사</label><select></select></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
describe('adminPaymentMethodBrandInjector (tosspayments)', () => {
|
||||
beforeEach(() => {
|
||||
document.documentElement.lang = 'ko';
|
||||
window.history.pushState({}, '', '/admin/ecommerce/settings?tab=order_settings');
|
||||
document.body.innerHTML = `
|
||||
${desktopItem('toss-naverpay', '네이버페이 (토스페이먼츠)', '네이버페이 간편결제 — 토스페이먼츠를 통해 처리')}
|
||||
${mobileItem('toss-kakaopay', '카카오페이 (토스페이먼츠)')}
|
||||
${desktopItem('nice-naverpay', '네이버페이 (나이스페이먼츠)', '네이버페이로 결제 (나이스페이먼츠)')}
|
||||
${desktopItem('toss-card', '신용카드 (토스페이먼츠)', '신용·체크카드로 결제 — 토스페이먼츠를 통해 처리')}
|
||||
`;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetAdminPaymentMethodBrandInjectorForTests();
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('토스 간편결제 행의 기본 아이콘을 브랜드 배지로 바꾼다', () => {
|
||||
expect(syncRenderedAdminPaymentMethodBrands()).toBe(true);
|
||||
|
||||
const naverpay = document.querySelector<HTMLElement>('[data-test-item="toss-naverpay"]');
|
||||
expect(naverpay?.dataset.tossAdminPaymentMethod).toBe('toss_naverpay');
|
||||
expect(naverpay?.querySelector('[data-toss-admin-payment-brand-mark="true"]')?.textContent).toBe('N');
|
||||
// 원래 아이콘은 치환되고, 드래그 핸들 아이콘은 남아야 한다
|
||||
expect(naverpay?.querySelector('[data-original-icon="true"]')).toBeNull();
|
||||
expect(naverpay?.querySelector('[data-drag-icon="true"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('모바일 카드 뷰에서도 동일하게 주입한다', () => {
|
||||
expect(syncRenderedAdminPaymentMethodBrands()).toBe(true);
|
||||
|
||||
const kakaopay = document.querySelector<HTMLElement>('[data-test-item="toss-kakaopay"]');
|
||||
expect(kakaopay?.dataset.tossAdminPaymentMethod).toBe('toss_kakaopay');
|
||||
expect(kakaopay?.querySelector('[data-toss-admin-payment-brand-mark="true"]')?.textContent).toBe('K');
|
||||
});
|
||||
|
||||
it('다른 PG 의 같은 브랜드 행은 건드리지 않는다 (플러그인 간 침범 금지)', () => {
|
||||
syncRenderedAdminPaymentMethodBrands();
|
||||
|
||||
const nice = document.querySelector<HTMLElement>('[data-test-item="nice-naverpay"]');
|
||||
expect(nice?.dataset.tossAdminPaymentMethod).toBeUndefined();
|
||||
expect(nice?.querySelector('[data-original-icon="true"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('브랜드가 아닌 수단(카드)은 기본 아이콘을 유지한다', () => {
|
||||
syncRenderedAdminPaymentMethodBrands();
|
||||
|
||||
const card = document.querySelector<HTMLElement>('[data-test-item="toss-card"]');
|
||||
expect(card?.dataset.tossAdminPaymentMethod).toBeUndefined();
|
||||
expect(card?.querySelector('[data-original-icon="true"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('주문설정 화면이 아니면 아무것도 바꾸지 않는다', () => {
|
||||
window.history.pushState({}, '', '/admin/ecommerce/settings?tab=shipping');
|
||||
|
||||
expect(syncRenderedAdminPaymentMethodBrands()).toBe(false);
|
||||
expect(document.querySelector('[data-toss-admin-payment-brand-mark="true"]')).toBeNull();
|
||||
});
|
||||
|
||||
/**
|
||||
* 탭 전환 후 복귀 시 배지 재주입 (브라우저 실측으로 발견한 회귀).
|
||||
*
|
||||
* 환경설정 화면의 탭 전환은 pushState 가 아니라 **replaceState** 로 URL 만 바꾼다
|
||||
* (탭 왕복 1회에 push 0 / replace 2 실측). pushState/popstate 만 후킹하던 기존 구현은
|
||||
* 다른 탭에 갔다 돌아왔을 때 onRouteChange 가 발화하지 않아, 행은 다시 렌더되는데
|
||||
* 배지만 사라진 채로 남았다 — 사용자에게는 아이콘이 회색으로 되돌아간 것으로 보인다.
|
||||
*/
|
||||
it('replaceState 로 탭을 전환했다 돌아와도 배지를 재주입한다', async () => {
|
||||
installAdminPaymentMethodBrandInjector();
|
||||
expect(document.querySelectorAll('[data-toss-admin-payment-brand-mark="true"]').length).toBeGreaterThan(0);
|
||||
|
||||
// 다른 탭으로 이동 (replaceState) — 화면이 갈아끼워지며 배지가 사라진 상태를 재현
|
||||
window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=shipping');
|
||||
document.body.innerHTML = '';
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
|
||||
// 주문설정으로 복귀 (replaceState) + 행 재렌더
|
||||
document.body.innerHTML = desktopItem('toss-naverpay', '네이버페이 (토스페이먼츠)', '네이버페이 간편결제 — 토스페이먼츠를 통해 처리');
|
||||
window.history.replaceState({}, '', '/admin/ecommerce/settings?tab=order_settings');
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
|
||||
expect(
|
||||
document.querySelectorAll('[data-toss-admin-payment-brand-mark="true"]').length,
|
||||
'replaceState 복귀 후 배지가 재주입되지 않았다',
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
+17
-2
@@ -261,13 +261,28 @@ describe('requestPaymentHandler', () => {
|
||||
expect(capturedPayload.virtualAccount.cashReceipt).toEqual({ type: '소득공제' });
|
||||
});
|
||||
|
||||
it('간편결제 선택 시 CARD + easyPay provider', async () => {
|
||||
it('간편결제 선택 시 CARD + card.easyPay 자체창 직행 (flowMode DIRECT)', async () => {
|
||||
setupSdk(baseConfig());
|
||||
|
||||
await requestPaymentHandler({ params: { pgPaymentData: pgData(), paymentMethod: 'toss_kakaopay' } });
|
||||
|
||||
// 토스 v2 결제창 계약: 간편결제 자체창은 card 객체 안의 easyPay + flowMode DIRECT.
|
||||
// top-level easyPay 키는 SDK 가 읽지 않아 통합결제창이 열린다 (회귀 pin).
|
||||
expect(capturedPayload.method).toBe('CARD');
|
||||
expect(capturedPayload.easyPay).toEqual({ provider: '카카오페이' });
|
||||
expect(capturedPayload.card.flowMode).toBe('DIRECT');
|
||||
expect(capturedPayload.card.easyPay).toBe('카카오페이');
|
||||
expect(capturedPayload.easyPay).toBeUndefined();
|
||||
});
|
||||
|
||||
it('일반 카드 선택 시 flowMode DEFAULT + easyPay 부재 (통합결제창)', async () => {
|
||||
setupSdk(baseConfig());
|
||||
|
||||
await requestPaymentHandler({ params: { pgPaymentData: pgData(), paymentMethod: 'toss_card' } });
|
||||
|
||||
expect(capturedPayload.method).toBe('CARD');
|
||||
expect(capturedPayload.card.flowMode).toBe('DEFAULT');
|
||||
expect('easyPay' in capturedPayload.card).toBe(false);
|
||||
expect(capturedPayload.easyPay).toBeUndefined();
|
||||
});
|
||||
|
||||
it('에스크로 on 이면 가상계좌 useEscrow=true', async () => {
|
||||
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* 관리자 주문설정 결제수단 목록의 브랜드 마크 주입.
|
||||
*
|
||||
* 관리자 레이아웃(`_payment_methods_list.json` / `_payment_methods_cards.json`)은 결제수단
|
||||
* 아이콘 열을 `Icon name={{$method._cached_icon}}` 하나로만 그린다 — 카탈로그의
|
||||
* `_cached_brand_mark` 를 읽는 노드가 없다(그 값을 렌더하는 곳은 사용자 템플릿 체크아웃뿐).
|
||||
* 그래서 각 PG 플러그인이 자기 결제수단 행의 아이콘을 브랜드 배지로 치환하는 관리자 전용
|
||||
* injector 를 직접 싣는다. 토스에만 이 파일이 없어 간편결제가 회색 기본 아이콘으로 보였다.
|
||||
*
|
||||
* 구조는 다른 PG 플러그인(nicepayments·kginicis)의 동명 파일과 동일하게 맞춘다 — 행 판별은
|
||||
* 라벨 텍스트, 치환 대상은 드래그 핸들/컨트롤을 제외한 첫 아이콘, 재적용은 MutationObserver
|
||||
* + 폴링이다. 배지 색·문자는 같은 브랜드가 어느 PG 를 통하든 같아야 하므로 서버측
|
||||
* `RegisterTossPaymentMethodsListener::METHOD_PRESENTATION` 의 `brand_mark` 와 값을 맞춘다.
|
||||
*/
|
||||
const PLUGIN_ID = 'sirsoft-tosspayments';
|
||||
const FLAG = '__tossAdminPaymentMethodBrandInjectorInstalled';
|
||||
const LISTENER_FLAG = '__tossAdminPaymentMethodBrandSyncListenerAttached';
|
||||
const ADMIN_SETTINGS_RE = /^\/admin\/ecommerce\/settings\/?$/;
|
||||
const MARK_SELECTOR = '[data-toss-admin-payment-brand-mark="true"]';
|
||||
const SYNC_RETRY_INTERVAL_MS = 200;
|
||||
const SYNC_RETRY_ATTEMPTS = 120;
|
||||
|
||||
interface AdminPaymentBrandDefinition {
|
||||
id: string;
|
||||
labels: string[];
|
||||
shortLabels: string[];
|
||||
markLines: string[];
|
||||
markClassName: string;
|
||||
}
|
||||
|
||||
const ADMIN_PAYMENT_BRAND_DEFINITIONS: AdminPaymentBrandDefinition[] = [
|
||||
{
|
||||
id: 'toss_tosspay',
|
||||
labels: ['토스페이 (토스페이먼츠)', '토스페이 간편결제 — 토스페이먼츠를 통해 처리', 'TossPay (TossPayments)', 'TossPay easy payment — processed via TossPayments'],
|
||||
shortLabels: [],
|
||||
markLines: ['T'],
|
||||
markClassName: 'bg-blue-500 text-white',
|
||||
},
|
||||
{
|
||||
id: 'toss_kakaopay',
|
||||
labels: ['카카오페이 (토스페이먼츠)', '카카오페이 간편결제 — 토스페이먼츠를 통해 처리', 'KakaoPay (TossPayments)', 'KakaoPay easy payment — processed via TossPayments'],
|
||||
shortLabels: [],
|
||||
markLines: ['K'],
|
||||
markClassName: 'bg-yellow-400 text-gray-950',
|
||||
},
|
||||
{
|
||||
id: 'toss_naverpay',
|
||||
labels: ['네이버페이 (토스페이먼츠)', '네이버페이 간편결제 — 토스페이먼츠를 통해 처리', 'NaverPay (TossPayments)', 'NaverPay easy payment — processed via TossPayments'],
|
||||
shortLabels: [],
|
||||
markLines: ['N'],
|
||||
markClassName: 'bg-green-500 text-white',
|
||||
},
|
||||
{
|
||||
id: 'toss_payco',
|
||||
labels: ['페이코 (토스페이먼츠)', '페이코 간편결제 — 토스페이먼츠를 통해 처리', 'PAYCO (TossPayments)', 'PAYCO easy payment — processed via TossPayments'],
|
||||
shortLabels: [],
|
||||
markLines: ['P'],
|
||||
markClassName: 'bg-red-500 text-white',
|
||||
},
|
||||
{
|
||||
id: 'toss_samsungpay',
|
||||
labels: ['삼성페이 (토스페이먼츠)', '삼성페이 간편결제 — 토스페이먼츠를 통해 처리', 'Samsung Pay (TossPayments)', 'Samsung Pay easy payment — processed via TossPayments'],
|
||||
shortLabels: [],
|
||||
markLines: ['S'],
|
||||
markClassName: 'bg-blue-600 text-white',
|
||||
},
|
||||
];
|
||||
|
||||
let observer: MutationObserver | null = null;
|
||||
let retryTimer: number | null = null;
|
||||
let syncQueued = false;
|
||||
|
||||
function windowRecord(): Record<string, unknown> {
|
||||
return window as unknown as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function isOrderSettingsPage(): boolean {
|
||||
if (!ADMIN_SETTINGS_RE.test(window.location.pathname)) return false;
|
||||
|
||||
const tab = new URLSearchParams(window.location.search).get('tab');
|
||||
return tab === null || tab === '' || tab === 'order_settings';
|
||||
}
|
||||
|
||||
function classNameOf(element: Element): string {
|
||||
return element instanceof HTMLElement ? element.className : '';
|
||||
}
|
||||
|
||||
function comparableText(value: string | null | undefined): string {
|
||||
return (value ?? '')
|
||||
.replace(//g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isPaymentMethodItem(element: HTMLElement): boolean {
|
||||
const className = classNameOf(element);
|
||||
|
||||
return className.includes('excel-card')
|
||||
|| (className.includes('flex-center') && className.includes('border') && className.includes('gap-4'));
|
||||
}
|
||||
|
||||
function findTitleElement(item: HTMLElement): HTMLElement | null {
|
||||
return item.querySelector<HTMLElement>('.font-medium');
|
||||
}
|
||||
|
||||
function findDefinitionForItem(item: HTMLElement): AdminPaymentBrandDefinition | null {
|
||||
const title = comparableText(findTitleElement(item)?.textContent);
|
||||
const text = comparableText(item.textContent);
|
||||
|
||||
return ADMIN_PAYMENT_BRAND_DEFINITIONS.find((definition) => (
|
||||
definition.shortLabels.some((label) => title === label)
|
||||
|| definition.labels.some((label) => text.includes(label))
|
||||
)) ?? null;
|
||||
}
|
||||
|
||||
function findPaymentMethodItems(root: ParentNode): HTMLElement[] {
|
||||
return Array.from(root.querySelectorAll<HTMLElement>('div'))
|
||||
.filter(isPaymentMethodItem);
|
||||
}
|
||||
|
||||
function findPaymentIcon(item: HTMLElement): Element | null {
|
||||
const icons = Array.from(item.querySelectorAll<Element>('svg, [data-icon], i'));
|
||||
|
||||
return icons.find((element) => (
|
||||
!element.closest('[data-drag-handle]')
|
||||
&& !element.closest(MARK_SELECTOR)
|
||||
&& !element.closest('.row-stack, .excel-card-body, button, select, [role="switch"]')
|
||||
)) ?? null;
|
||||
}
|
||||
|
||||
function applyBrandMarkContent(mark: HTMLSpanElement, definition: AdminPaymentBrandDefinition): void {
|
||||
mark.dataset.tossAdminPaymentBrandMark = 'true';
|
||||
mark.dataset.tossAdminPaymentMethod = definition.id;
|
||||
mark.setAttribute('aria-hidden', 'true');
|
||||
mark.className = `inline-flex items-center justify-center rounded-lg font-bold ${definition.markClassName}`;
|
||||
mark.style.width = '32px';
|
||||
mark.style.height = '32px';
|
||||
mark.style.flex = '0 0 32px';
|
||||
mark.style.lineHeight = '1';
|
||||
mark.style.fontSize = definition.markLines.join('').length > 2 ? '9px' : '12px';
|
||||
|
||||
const expectedText = definition.markLines.join('');
|
||||
if (mark.textContent !== expectedText) {
|
||||
mark.textContent = definition.markLines[0] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
function createBrandMark(definition: AdminPaymentBrandDefinition): HTMLSpanElement {
|
||||
const mark = document.createElement('span');
|
||||
applyBrandMarkContent(mark, definition);
|
||||
|
||||
return mark;
|
||||
}
|
||||
|
||||
function syncBrandMark(item: HTMLElement, definition: AdminPaymentBrandDefinition): boolean {
|
||||
const existing = item.querySelector<HTMLElement>(MARK_SELECTOR);
|
||||
if (existing instanceof HTMLSpanElement) {
|
||||
item.dataset.tossAdminPaymentMethod = definition.id;
|
||||
applyBrandMarkContent(existing, definition);
|
||||
return true;
|
||||
}
|
||||
|
||||
const mark = createBrandMark(definition);
|
||||
const icon = findPaymentIcon(item);
|
||||
if (icon && icon.parentElement) {
|
||||
item.dataset.tossAdminPaymentMethod = definition.id;
|
||||
icon.replaceWith(mark);
|
||||
return true;
|
||||
}
|
||||
|
||||
const title = findTitleElement(item);
|
||||
if (title && title.parentElement) {
|
||||
item.dataset.tossAdminPaymentMethod = definition.id;
|
||||
title.parentElement.insertBefore(mark, title);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function syncRenderedAdminPaymentMethodBrands(root: ParentNode = document): boolean {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return false;
|
||||
if (!isOrderSettingsPage()) return false;
|
||||
|
||||
let matched = false;
|
||||
|
||||
findPaymentMethodItems(root).forEach((item) => {
|
||||
const definition = findDefinitionForItem(item);
|
||||
if (!definition) return;
|
||||
|
||||
if (syncBrandMark(item, definition)) {
|
||||
matched = true;
|
||||
}
|
||||
});
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
function queueSync(): void {
|
||||
if (syncQueued) return;
|
||||
syncQueued = true;
|
||||
|
||||
window.setTimeout(() => {
|
||||
syncQueued = false;
|
||||
syncRenderedAdminPaymentMethodBrands();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function stopRetries(): void {
|
||||
if (retryTimer === null) return;
|
||||
|
||||
window.clearInterval(retryTimer);
|
||||
retryTimer = null;
|
||||
}
|
||||
|
||||
function startSync(): void {
|
||||
if (!isOrderSettingsPage()) return;
|
||||
|
||||
stopRetries();
|
||||
syncRenderedAdminPaymentMethodBrands();
|
||||
|
||||
let attempts = 0;
|
||||
retryTimer = window.setInterval(() => {
|
||||
attempts += 1;
|
||||
syncRenderedAdminPaymentMethodBrands();
|
||||
|
||||
if (attempts >= SYNC_RETRY_ATTEMPTS) {
|
||||
stopRetries();
|
||||
}
|
||||
}, SYNC_RETRY_INTERVAL_MS);
|
||||
|
||||
const body = document.body as HTMLElement & Record<string, unknown>;
|
||||
if (body[LISTENER_FLAG]) return;
|
||||
body[LISTENER_FLAG] = true;
|
||||
|
||||
observer = new MutationObserver(() => queueSync());
|
||||
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
||||
}
|
||||
|
||||
function onRouteChange(): void {
|
||||
if (isOrderSettingsPage()) {
|
||||
startSync();
|
||||
return;
|
||||
}
|
||||
|
||||
stopRetries();
|
||||
}
|
||||
|
||||
export function installAdminPaymentMethodBrandInjector(): void {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
||||
const w = windowRecord();
|
||||
if (w[FLAG]) return;
|
||||
w[FLAG] = true;
|
||||
|
||||
console.info(`[${PLUGIN_ID}] admin payment method brand sync installed`);
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => startSync());
|
||||
} else {
|
||||
startSync();
|
||||
}
|
||||
|
||||
const origPush = history.pushState.bind(history);
|
||||
history.pushState = (...args: Parameters<typeof history.pushState>) => {
|
||||
origPush(...args);
|
||||
window.setTimeout(() => onRouteChange(), 200);
|
||||
};
|
||||
|
||||
// replaceState 도 후킹한다 — 환경설정 화면의 탭 전환은 pushState 가 아니라
|
||||
// replaceState 로 URL 만 바꾼다(실측: 탭 왕복 1회에 push 0 / replace 2). pushState 만
|
||||
// 후킹하면 다른 탭에 갔다 돌아왔을 때 onRouteChange 가 발화하지 않아 배지가 사라진 채로
|
||||
// 남는다(행은 그대로 렌더되므로 화면상 아이콘만 회색으로 되돌아간다).
|
||||
const origReplace = history.replaceState.bind(history);
|
||||
history.replaceState = (...args: Parameters<typeof history.replaceState>) => {
|
||||
origReplace(...args);
|
||||
window.setTimeout(() => onRouteChange(), 200);
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', () => window.setTimeout(() => onRouteChange(), 200));
|
||||
}
|
||||
|
||||
export function resetAdminPaymentMethodBrandInjectorForTests(): void {
|
||||
observer?.disconnect();
|
||||
observer = null;
|
||||
stopRetries();
|
||||
syncQueued = false;
|
||||
delete windowRecord()[FLAG];
|
||||
if (document.body) {
|
||||
delete ((document.body as HTMLElement & Record<string, unknown>)[LISTENER_FLAG]);
|
||||
}
|
||||
}
|
||||
@@ -275,9 +275,12 @@ export async function requestPaymentHandler(action: any, _context?: any): Promis
|
||||
useCardPoint: false,
|
||||
useAppCardOnly: false,
|
||||
};
|
||||
// 간편결제(easyPay)는 CARD method 에 easyPay provider 를 실어 호출한다.
|
||||
// 간편결제는 card 객체 안에 easyPay(간편결제사 코드) + flowMode DIRECT 로
|
||||
// 자체창을 직행 호출한다 (토스 v2 결제창 계약 — 카드사/간편결제 자체창 연동).
|
||||
// top-level easyPay 키는 SDK 파라미터가 아니라서 무시되고 통합결제창이 열린다.
|
||||
if (easyPay) {
|
||||
requestPayload.easyPay = { provider: easyPay };
|
||||
requestPayload.card.flowMode = 'DIRECT';
|
||||
requestPayload.card.easyPay = easyPay;
|
||||
}
|
||||
} else if (method === 'VIRTUAL_ACCOUNT') {
|
||||
// 가상계좌 — 에스크로 적용 대상 (escrowProducts 는 에스크로 사용 시 필수)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { handlerMap } from './handlers';
|
||||
import { installAdminPaymentMethodBrandInjector } from './adminPaymentMethodBrandInjector';
|
||||
|
||||
const PLUGIN_IDENTIFIER = 'sirsoft-tosspayments';
|
||||
|
||||
@@ -62,6 +63,10 @@ function registerHandlers(): number {
|
||||
* ActionDispatcher가 아직 준비되지 않았으면 최대 5초간 재시도합니다.
|
||||
*/
|
||||
function initPlugin(): void {
|
||||
// 관리자 주문설정의 브랜드 마크 주입 — 핸들러 등록 성공 여부와 무관하게 설치한다
|
||||
// (관리자 화면은 결제 핸들러를 쓰지 않으므로 ActionDispatcher 준비를 기다릴 이유가 없다).
|
||||
installAdminPaymentMethodBrandInjector();
|
||||
|
||||
const doInit = () => {
|
||||
const count = registerHandlers();
|
||||
|
||||
|
||||
+52
-10
@@ -23,21 +23,47 @@ class RegisterTossPaymentMethodsListener implements HookListenerInterface
|
||||
|
||||
private const PLUGIN_IDENTIFIER = 'sirsoft-tosspayments';
|
||||
|
||||
/** RegisterPgProviderListener 가 등록하는 PG 제공자 id 와 동일해야 한다. */
|
||||
private const PG_PROVIDER_ID = 'tosspayments';
|
||||
|
||||
/**
|
||||
* toss_* id → 아이콘·다국어 키.
|
||||
* toss_* id → 아이콘·브랜드 마크.
|
||||
*
|
||||
* @var array<string, array{icon:string}>
|
||||
* 간편결제 수단은 brand_mark 로 브랜드 색상 배지를 노출한다. 같은 브랜드는 어느 PG 를
|
||||
* 통하든 같은 배지로 보여야 하므로 text/class 값을 다른 PG 플러그인과 동일하게 맞춘다
|
||||
* (naverpay N/녹색, kakaopay K/노랑, samsungpay S/파랑, payco P/빨강). 미선언 시
|
||||
* 이커머스가 `_cached_brand_mark` 를 null 로 채워 회색 기본 아이콘만 표시된다.
|
||||
*
|
||||
* 카드·가상계좌·계좌이체·휴대폰은 브랜드가 아니라 결제 형태이므로 코어 빌트인 수단과
|
||||
* 동일하게 아이콘만 둔다.
|
||||
*
|
||||
* @var array<string, array{icon:string, brand_mark?:array{text:string, class:string}}>
|
||||
*/
|
||||
private const METHOD_PRESENTATION = [
|
||||
'toss_card' => ['icon' => 'credit-card'],
|
||||
'toss_virtual_account' => ['icon' => 'building-columns'],
|
||||
'toss_transfer' => ['icon' => 'money-bill-transfer'],
|
||||
'toss_mobile_phone' => ['icon' => 'mobile-screen-button'],
|
||||
'toss_tosspay' => ['icon' => 'wallet'],
|
||||
'toss_kakaopay' => ['icon' => 'wallet'],
|
||||
'toss_naverpay' => ['icon' => 'wallet'],
|
||||
'toss_payco' => ['icon' => 'wallet'],
|
||||
'toss_samsungpay' => ['icon' => 'mobile-screen-button'],
|
||||
'toss_tosspay' => [
|
||||
'icon' => 'wallet',
|
||||
'brand_mark' => ['text' => 'T', 'class' => 'bg-blue-500 text-white'],
|
||||
],
|
||||
'toss_kakaopay' => [
|
||||
'icon' => 'wallet',
|
||||
'brand_mark' => ['text' => 'K', 'class' => 'bg-yellow-400 text-gray-950'],
|
||||
],
|
||||
'toss_naverpay' => [
|
||||
'icon' => 'wallet',
|
||||
'brand_mark' => ['text' => 'N', 'class' => 'bg-green-500 text-white'],
|
||||
],
|
||||
'toss_payco' => [
|
||||
'icon' => 'wallet',
|
||||
'brand_mark' => ['text' => 'P', 'class' => 'bg-red-500 text-white'],
|
||||
],
|
||||
'toss_samsungpay' => [
|
||||
'icon' => 'mobile-screen-button',
|
||||
'brand_mark' => ['text' => 'S', 'class' => 'bg-blue-600 text-white'],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -118,7 +144,7 @@ class RegisterTossPaymentMethodsListener implements HookListenerInterface
|
||||
$nameKey = "sirsoft-tosspayments::payment_methods.{$id}.name";
|
||||
$descriptionKey = "sirsoft-tosspayments::payment_methods.{$id}.description";
|
||||
|
||||
return [
|
||||
$entry = [
|
||||
'id' => $id,
|
||||
'name' => [
|
||||
'ko' => __($nameKey, [], 'ko'),
|
||||
@@ -131,8 +157,15 @@ class RegisterTossPaymentMethodsListener implements HookListenerInterface
|
||||
'icon' => self::METHOD_PRESENTATION[$id]['icon'] ?? 'credit-card',
|
||||
'source' => 'plugin:sirsoft-tosspayments',
|
||||
'defaults' => [
|
||||
// PG 선택 불필요 — payment_handler 정공법으로 토스 결제 흐름이 발화한다.
|
||||
'pg_provider' => null,
|
||||
// 토스 결제수단은 토스 결제창을 통해서만 처리되므로 PG 를 자기 자신으로 고정한다.
|
||||
// null 로 두면 코어가 PG 없는 결제수단으로 오인해 결제 실패 주문에 관리자 알림이
|
||||
// 발송되고 임시주문이 삭제되어 재결제가 막힌다(#475). 또한 관리자 화면이 "PG 고정"
|
||||
// 배지 대신 선택지가 비어 있는 PG 셀렉트를 그린다 — 토스 PG 는 자기 결제수단 id 를
|
||||
// supported_methods 로 선언하지 않으므로 고를 수 있는 항목이 하나도 없다.
|
||||
'pg_provider' => self::PG_PROVIDER_ID,
|
||||
'pg_locked' => true,
|
||||
'needs_pg' => true,
|
||||
'refund_method' => 'pg',
|
||||
'is_active' => false,
|
||||
'min_order_amount' => 0,
|
||||
'stock_deduction_timing' => 'payment_complete',
|
||||
@@ -141,6 +174,15 @@ class RegisterTossPaymentMethodsListener implements HookListenerInterface
|
||||
'core_payment_method' => self::TOSS_METHOD_MAP[$id]['core'],
|
||||
],
|
||||
];
|
||||
|
||||
// 브랜드 마크는 간편결제 수단에만 있다. 없는 수단에 null 키를 남기지 않는다 —
|
||||
// 이커머스 병합부가 `$definition['brand_mark'] ?? null` 로 읽으므로 결과는 같지만,
|
||||
// 키 유무가 곧 "브랜드 수단인가" 의 선언이라 다른 PG 플러그인과 형식을 맞춘다.
|
||||
if (isset(self::METHOD_PRESENTATION[$id]['brand_mark'])) {
|
||||
$entry['brand_mark'] = self::METHOD_PRESENTATION[$id]['brand_mark'];
|
||||
}
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+106
-2
@@ -128,8 +128,9 @@ class RegisterTossPaymentMethodsListenerTest extends PluginTestCase
|
||||
// 가상계좌 → vbank, 간편결제 → card
|
||||
$this->assertSame('vbank', $byId['toss_virtual_account']['defaults']['core_payment_method']);
|
||||
$this->assertSame('card', $byId['toss_kakaopay']['defaults']['core_payment_method']);
|
||||
// PG 선택 불필요
|
||||
$this->assertNull($byId['toss_virtual_account']['defaults']['pg_provider']);
|
||||
// PG 는 토스로 고정된다 (#475 전환). 과거에는 null 이었고 그것이 관리자 화면의
|
||||
// 빈 PG 셀렉트와 서버측 "PG 없는 결제수단" 오인의 원인이었다.
|
||||
$this->assertSame('tosspayments', $byId['toss_virtual_account']['defaults']['pg_provider']);
|
||||
}
|
||||
|
||||
public function test_all_toggles_off_injects_nothing(): void
|
||||
@@ -140,4 +141,107 @@ class RegisterTossPaymentMethodsListenerTest extends PluginTestCase
|
||||
|
||||
$this->assertCount(3, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 토스 결제수단은 PG 를 자기 자신으로 고정 선언해야 한다 (#475 전환 누락 회귀).
|
||||
*
|
||||
* 배경: #475 가 확장 결제수단에 pg_locked/needs_pg/refund_method 선언을 도입할 때
|
||||
* 토스는 3일 먼저 들어와 있었음에도 전환 대상에서 빠졌다. 그 결과 pg_provider 가 null 인
|
||||
* 채로 needs_pg 만 참이 되어, 관리자 화면이 "PG 고정" 배지 대신 **선택지가 하나도 없는**
|
||||
* PG 셀렉트를 그렸다(토스 PG 는 toss_* id 를 supported_methods 로 선언하지 않는다).
|
||||
* 서버 쪽으로도 PG 없는 결제수단으로 오인되어 결제 실패 시 임시주문이 삭제될 수 있었다.
|
||||
*/
|
||||
public function test_all_methods_declare_pg_locked_to_self(): void
|
||||
{
|
||||
$this->mockSettings([
|
||||
'order_sheet_mode' => true,
|
||||
'method_card' => true,
|
||||
'method_virtual_account' => true,
|
||||
'method_transfer' => true,
|
||||
'method_mobile_phone' => true,
|
||||
'method_tosspay' => true,
|
||||
'method_kakaopay' => true,
|
||||
'method_naverpay' => true,
|
||||
'method_payco' => true,
|
||||
'method_samsungpay' => true,
|
||||
]);
|
||||
|
||||
$injected = collect($this->listener->injectTossMethods($this->builtinMethods()))
|
||||
->filter(fn ($m) => str_starts_with($m['id'] ?? '', 'toss_'));
|
||||
|
||||
$this->assertGreaterThan(0, $injected->count(), '토스 결제수단이 주입되지 않았다');
|
||||
|
||||
foreach ($injected as $method) {
|
||||
$defaults = $method['defaults'] ?? [];
|
||||
$id = $method['id'];
|
||||
|
||||
$this->assertSame('tosspayments', $defaults['pg_provider'] ?? null, "{$id}: PG 미고정");
|
||||
$this->assertTrue($defaults['pg_locked'] ?? false, "{$id}: pg_locked 미선언 — 관리자가 다른 PG 로 바꿀 수 있게 된다");
|
||||
$this->assertTrue($defaults['needs_pg'] ?? false, "{$id}: needs_pg 미선언");
|
||||
$this->assertSame('pg', $defaults['refund_method'] ?? null, "{$id}: refund_method 미선언");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 간편결제 수단은 브랜드 마크를 선언해야 한다.
|
||||
*
|
||||
* 회귀 배경: 토스 수단만 brand_mark 를 선언하지 않아, 이커머스 병합부가
|
||||
* `_cached_brand_mark` 를 null 로 채우고 관리자·체크아웃 화면이 브랜드 배지 대신
|
||||
* 회색 기본 아이콘을 그렸다. 같은 브랜드가 다른 PG 를 통할 때와 다르게 보였다.
|
||||
*
|
||||
* 같은 브랜드는 어느 PG 를 통하든 같은 배지여야 하므로 text/class 를 함께 고정한다.
|
||||
*/
|
||||
public function test_easy_pay_methods_declare_brand_mark_consistent_with_other_pg(): void
|
||||
{
|
||||
$this->mockSettings([
|
||||
'order_sheet_mode' => true,
|
||||
'method_tosspay' => true,
|
||||
'method_kakaopay' => true,
|
||||
'method_naverpay' => true,
|
||||
'method_payco' => true,
|
||||
'method_samsungpay' => true,
|
||||
]);
|
||||
|
||||
$byId = collect($this->listener->injectTossMethods($this->builtinMethods()))->keyBy('id');
|
||||
|
||||
// 다른 PG 플러그인(nicepayments·nhnkcp)이 같은 브랜드에 쓰는 값과 동일해야 한다.
|
||||
$expected = [
|
||||
'toss_naverpay' => ['text' => 'N', 'class' => 'bg-green-500 text-white'],
|
||||
'toss_kakaopay' => ['text' => 'K', 'class' => 'bg-yellow-400 text-gray-950'],
|
||||
'toss_samsungpay' => ['text' => 'S', 'class' => 'bg-blue-600 text-white'],
|
||||
'toss_payco' => ['text' => 'P', 'class' => 'bg-red-500 text-white'],
|
||||
'toss_tosspay' => ['text' => 'T', 'class' => 'bg-blue-500 text-white'],
|
||||
];
|
||||
|
||||
foreach ($expected as $id => $mark) {
|
||||
$this->assertArrayHasKey($id, $byId, "{$id} 가 주입되지 않았다");
|
||||
$this->assertSame(
|
||||
$mark,
|
||||
$byId[$id]['brand_mark'] ?? null,
|
||||
"{$id} 의 브랜드 마크가 다른 PG 와 다르다 — 같은 브랜드가 PG 마다 달라 보인다"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드·가상계좌·계좌이체·휴대폰은 브랜드가 아니라 결제 형태이므로 마크를 두지 않는다.
|
||||
* (코어 빌트인 수단과 동일한 표현)
|
||||
*/
|
||||
public function test_non_brand_methods_have_no_brand_mark(): void
|
||||
{
|
||||
$this->mockSettings([
|
||||
'order_sheet_mode' => true,
|
||||
'method_card' => true,
|
||||
'method_virtual_account' => true,
|
||||
'method_transfer' => true,
|
||||
'method_mobile_phone' => true,
|
||||
]);
|
||||
|
||||
$byId = collect($this->listener->injectTossMethods($this->builtinMethods()))->keyBy('id');
|
||||
|
||||
foreach (['toss_card', 'toss_virtual_account', 'toss_transfer', 'toss_mobile_phone'] as $id) {
|
||||
$this->assertArrayHasKey($id, $byId, "{$id} 가 주입되지 않았다");
|
||||
$this->assertArrayNotHasKey('brand_mark', $byId[$id], "{$id} 는 브랜드 수단이 아니다");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ All notable changes to this plugin will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [1.0.2] - 2026-08-01
|
||||
## [1.0.2] - 2026-08-10
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ All notable changes to this plugin will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [1.0.0] - 2026-07-28
|
||||
## [1.0.0] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
+4
-4
@@ -1,14 +1,14 @@
|
||||
var dM=Object.defineProperty;var cM=(dn,i,u)=>i in dn?dM(dn,i,{enumerable:!0,configurable:!0,writable:!0,value:u}):dn[i]=u;var Ti=(dn,i,u)=>cM(dn,typeof i!="symbol"?i+"":i,u);(function(dn,i,u,Cn){"use strict";function Lt(){const e=typeof window<"u"?window.G7Core?.__runtime:void 0;if(!e)throw new Error("[layout-editor] window.G7Core.__runtime 이 없습니다. 코어 번들(template-engine.min.js)이 편집기 번들보다 먼저 로드되어야 합니다.");return e}const Di=Lt();Di.TranslationProvider;const tt=Di.useTranslation;Di.TranslationReactContext;function rt(){return typeof window>"u"?null:window.__g7Devtools??null}function Sl(e,t){const n=rt();if(!n?.trackEditorState)return;const o={templateIdentifier:e.templateIdentifier,editMode:e.editMode,selectedRoutePath:e.selectedRoute?.path??null,selectedLayoutName:e.selectedRoute?.layoutName??null,routeTreeSize:e.routeTree.length,isRouteTreeCollapsed:e.isRouteTreeCollapsed,locale:e.locale,previewDevice:e.previewDevice,previewZoom:e.previewZoom,lastAction:t,timestamp:Date.now()};try{n.trackEditorState(o)}catch{}}function Vh(){const e=rt();if(e?.clearEditorStateData)try{e.clearEditorStateData()}catch{}}function Uh(e){const t=rt();if(t?.trackEditorSampleMatch)try{t.trackEditorSampleMatch(e)}catch{}}function qh(){const e=rt();if(e?.clearEditorSampleData)try{e.clearEditorSampleData()}catch{}}function Gh(e){const t=rt();if(t?.trackEditorHistoryEntry)try{t.trackEditorHistoryEntry(e)}catch{}}function Yh(){const e=rt();if(e?.clearEditorHistoryData)try{e.clearEditorHistoryData()}catch{}}function Cl(e){const t=rt();if(t?.trackEditorSelection)try{t.trackEditorSelection(e)}catch{}}function Xh(){const e=rt();if(e?.clearEditorSelectionData)try{e.clearEditorSelectionData()}catch{}}function sr(e){const t=rt();if(t?.trackEditorPropertyPatch)try{t.trackEditorPropertyPatch(e)}catch{}}function $n(e){const t=rt();if(t?.trackEditorDnd)try{t.trackEditorDnd(e)}catch{}}function Jh(){const e=rt();if(e?.clearEditorDndData)try{e.clearEditorDndData()}catch{}}function st(e){const t=rt();if(t?.trackEditorDocument)try{t.trackEditorDocument(e)}catch{}}function Zh(){const e=rt();if(e?.clearEditorDocumentData)try{e.clearEditorDocumentData()}catch{}}function Qh(e){const t=rt();if(t?.trackEditorSpecMerge)try{t.trackEditorSpecMerge(e)}catch{}}function Rh(){const e=rt();if(e?.clearEditorSpecMergeData)try{e.clearEditorSpecMergeData()}catch{}}function Ni(e){let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t+e.charCodeAt(n)>>>0;return t}function Et(e){const t=rt();if(t?.trackEditorI18n)try{t.trackEditorI18n(e)}catch{}}function ey(){const e=rt();if(e?.clearEditorI18nData)try{e.clearEditorI18nData()}catch{}}function $l(e){const t=rt();if(t?.trackPageState)try{t.trackPageState(e)}catch{}}function ty(){const e=rt();if(e?.clearPageStateData)try{e.clearPageStateData()}catch{}}const El="g7le.routeTree.collapsed";function ny(){if(typeof window>"u")return!1;try{return window.localStorage?.getItem(El)==="1"}catch{return!1}}function oy(e){if(!(typeof window>"u"))try{window.localStorage?.setItem(El,e?"1":"0")}catch{}}function ry(e,t){return{templateIdentifier:e,routeTree:[],routesError:null,selectedRoute:null,isRouteTreeCollapsed:ny(),editMode:"route",returnRoute:null,returnEditMode:null,locale:t,previewDevice:"desktop",previewZoom:1,previewCustomWidth:1024,previewColorScheme:"light",history:{canUndo:!1,canRedo:!1},isPaletteOpen:!1,availableStates:[],activeStateId:null,layoutVersions:{},extensionVersions:{}}}function iy(e,t){switch(t.type){case"SET_ROUTE_TREE":return{...e,routeTree:t.tree,routesError:null};case"SET_ROUTES_ERROR":return{...e,routesError:t.error};case"SELECT_ROUTE":return{...e,editMode:"route",selectedRoute:t.route,returnRoute:null,returnEditMode:null};case"TOGGLE_ROUTE_TREE":return{...e,isRouteTreeCollapsed:!e.isRouteTreeCollapsed};case"SET_LOCALE":return{...e,locale:t.locale};case"ENTER_BASE_EDIT":return{...e,editMode:"base",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,selectedRoute:{path:`__base__/${t.layoutName}`,layoutName:t.layoutName}};case"EXIT_BASE_EDIT":return{...e,editMode:"route",selectedRoute:e.returnRoute,returnRoute:null};case"ENTER_MODAL_EDIT":return{...e,editMode:"modal",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,selectedRoute:{path:`__modal__/${t.modalId}`,layoutName:t.hostLayout}};case"EXIT_MODAL_EDIT":return{...e,editMode:"route",selectedRoute:e.returnRoute,returnRoute:null};case"ENTER_EXTENSION_EDIT":return{...e,editMode:"extension",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,selectedRoute:{path:`__extension__/${t.extensionId}`,layoutName:t.extensionHost??null}};case"SET_RETURN_ROUTE":return e.editMode==="route"||e.returnRoute!==null?e:{...e,returnRoute:t.route};case"EXIT_EXTENSION_EDIT":return{...e,editMode:"route",selectedRoute:e.returnRoute,returnRoute:null};case"ENTER_ITERATION_ITEM_EDIT":return{...e,editMode:"iteration_item",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,returnEditMode:e.ediLine truncated
|
||||
var dM=Object.defineProperty;var cM=(dn,i,u)=>i in dn?dM(dn,i,{enumerable:!0,configurable:!0,writable:!0,value:u}):dn[i]=u;var Ti=(dn,i,u)=>cM(dn,typeof i!="symbol"?i+"":i,u);(function(dn,i,u,Cn){"use strict";function Lt(){const e=typeof window<"u"?window.G7Core?.__runtime:void 0;if(!e)throw new Error("[layout-editor] window.G7Core.__runtime 이 없습니다. 코어 번들(template-engine.min.js)이 편집기 번들보다 먼저 로드되어야 합니다.");return e}const Di=Lt();Di.TranslationProvider;const tt=Di.useTranslation;Di.TranslationReactContext;function rt(){return typeof window>"u"?null:window.__g7Devtools??null}function Sl(e,t){const n=rt();if(!n?.trackEditorState)return;const o={templateIdentifier:e.templateIdentifier,editMode:e.editMode,selectedRoutePath:e.selectedRoute?.path??null,selectedLayoutName:e.selectedRoute?.layoutName??null,routeTreeSize:e.routeTree.length,isRouteTreeCollapsed:e.isRouteTreeCollapsed,locale:e.locale,previewDevice:e.previewDevice,previewZoom:e.previewZoom,lastAction:t,timestamp:Date.now()};try{n.trackEditorState(o)}catch{}}function Vh(){const e=rt();if(e?.clearEditorStateData)try{e.clearEditorStateData()}catch{}}function Uh(e){const t=rt();if(t?.trackEditorSampleMatch)try{t.trackEditorSampleMatch(e)}catch{}}function qh(){const e=rt();if(e?.clearEditorSampleData)try{e.clearEditorSampleData()}catch{}}function Gh(e){const t=rt();if(t?.trackEditorHistoryEntry)try{t.trackEditorHistoryEntry(e)}catch{}}function Yh(){const e=rt();if(e?.clearEditorHistoryData)try{e.clearEditorHistoryData()}catch{}}function Cl(e){const t=rt();if(t?.trackEditorSelection)try{t.trackEditorSelection(e)}catch{}}function Xh(){const e=rt();if(e?.clearEditorSelectionData)try{e.clearEditorSelectionData()}catch{}}function sr(e){const t=rt();if(t?.trackEditorPropertyPatch)try{t.trackEditorPropertyPatch(e)}catch{}}function $n(e){const t=rt();if(t?.trackEditorDnd)try{t.trackEditorDnd(e)}catch{}}function Jh(){const e=rt();if(e?.clearEditorDndData)try{e.clearEditorDndData()}catch{}}function st(e){const t=rt();if(t?.trackEditorDocument)try{t.trackEditorDocument(e)}catch{}}function Zh(){const e=rt();if(e?.clearEditorDocumentData)try{e.clearEditorDocumentData()}catch{}}function Qh(e){const t=rt();if(t?.trackEditorSpecMerge)try{t.trackEditorSpecMerge(e)}catch{}}function Rh(){const e=rt();if(e?.clearEditorSpecMergeData)try{e.clearEditorSpecMergeData()}catch{}}function Ni(e){let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t+e.charCodeAt(n)>>>0;return t}function Et(e){const t=rt();if(t?.trackEditorI18n)try{t.trackEditorI18n(e)}catch{}}function ey(){const e=rt();if(e?.clearEditorI18nData)try{e.clearEditorI18nData()}catch{}}function $l(e){const t=rt();if(t?.trackPageState)try{t.trackPageState(e)}catch{}}function ty(){const e=rt();if(e?.clearPageStateData)try{e.clearPageStateData()}catch{}}const El="g7le.routeTree.collapsed";function ny(){if(typeof window>"u")return!1;try{return window.localStorage?.getItem(El)==="1"}catch{return!1}}function oy(e){if(!(typeof window>"u"))try{window.localStorage?.setItem(El,e?"1":"0")}catch{}}function ry(e,t){return{templateIdentifier:e,routeTree:[],routesError:null,selectedRoute:null,isRouteTreeCollapsed:ny(),editMode:"route",returnRoute:null,returnEditMode:null,locale:t,previewDevice:"desktop",previewZoom:1,previewCustomWidth:1024,previewColorScheme:"light",history:{canUndo:!1,canRedo:!1},isPaletteOpen:!1,availableStates:[],activeStateId:null,layoutVersions:{},extensionVersions:{}}}function iy(e,t){switch(t.type){case"SET_ROUTE_TREE":return{...e,routeTree:t.tree,routesError:null};case"SET_ROUTES_ERROR":return{...e,routesError:t.error};case"SELECT_ROUTE":return{...e,editMode:"route",selectedRoute:t.route,returnRoute:null,returnEditMode:null};case"TOGGLE_ROUTE_TREE":return{...e,isRouteTreeCollapsed:!e.isRouteTreeCollapsed};case"SET_LOCALE":return{...e,locale:t.locale};case"ENTER_BASE_EDIT":return{...e,editMode:"base",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,selectedRoute:{path:`__base__/${t.layoutName}`,layoutName:t.layoutName}};case"EXIT_BASE_EDIT":return{...e,editMode:"route",selectedRoute:e.returnRoute,returnRoute:null};case"ENTER_MODAL_EDIT":return{...e,editMode:"modal",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,selectedRoute:{path:`__modal__/${t.modalId}`,layoutName:t.hostLayout}};case"EXIT_MODAL_EDIT":return{...e,editMode:"route",selectedRoute:e.returnRoute,returnRoute:null};case"ENTER_EXTENSION_EDIT":return{...e,editMode:"extension",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,selectedRoute:{path:`__extension__/${t.extensionId}`,layoutName:t.extensionHost??null}};case"SET_RETURN_ROUTE":return e.editMode==="route"||e.returnRoute!==null?e:{...e,returnRoute:t.route};case"EXIT_EXTENSION_EDIT":return{...e,editMode:"route",selectedRoute:e.returnRoute,returnRoute:null};case"ENTER_ITERATION_ITEM_EDIT":return{...e,editMode:"iteration_item",returnRoute:t.returnRoute!==void 0?t.returnRoute:e.selectedRoute,returnEditMode:e.ediLine truncated
|
||||
"keyword": "",
|
||||
"page": 1
|
||||
}`,shapeErrorKey:"layout_editor.json_block.must_be_object",validate:_=>{const C=Object.keys(_??{}).filter(E=>!Is(E));return C.length?a("layout_editor.json_block.invalid_keys",{keys:C.join(", ")}):null}}):i.jsxs(i.Fragment,{children:[v.length===0?i.jsx("p",{"data-testid":`g7le-initstate-${e}-empty`,style:LS,children:a("layout_editor.initstate.no_values")}):v.map(_=>{const E=Lw(b,r,_)==="inherited",S=e==="isolated"&&l[_];return i.jsx(hS,{scope:e,itemKey:_,value:o[_],inherited:E,isOrphan:!!S,onChangeValue:T=>x(_,T),onRemove:()=>k(_),t:a},_)}),i.jsxs("div",{style:AS,children:[i.jsx("input",{type:"text","data-testid":`g7le-initstate-${e}-add-name`,value:d,placeholder:a("layout_editor.initstate.value_name"),onChange:_=>{c(_.target.value),y&&g(null)},onKeyDown:_=>{_.key==="Enter"&&(_.preventDefault(),w())},"aria-invalid":y?!0:void 0,style:{...TS,...y?DS:null}}),i.jsx("select",{"data-testid":`g7le-initstate-${e}-add-kind`,value:p,onChange:_=>f(_.target.value),"aria-label":a("layout_editor.initstate.value_kind"),style:zS,children:fS.map(_=>i.jsx("option",{value:_.kind,children:a(_.labelKey)},_.kind))}),i.jsxs("button",{type:"button","data-testid":`g7le-initstate-${e}-add`,onClick:w,style:MS,children:["+ ",a("layout_editor.initstate.add_value")]})]}),y?i.jsx("p",{"data-testid":`g7le-initstate-${e}-add-error`,style:NS,children:y}):null]})]})}function hS({scope:e,itemKey:t,value:n,inherited:o,isOrphan:r,onChangeValue:s,onRemove:a,t:l}){const[d,c]=u.useState(!1);let p;try{p=JSON.stringify(n,null,2)??String(n)}catch{p=String(n)}return i.jsxs("div",{"data-testid":`g7le-initstate-${e}-item-${t}`,style:SS,children:[i.jsxs("div",{style:CS,children:[o?i.jsx("span",{"data-testid":`g7le-initstate-inherited-${t}`,style:$S,children:"🔗"}):null,i.jsx("code",{style:ES,children:t}),i.jsx("span",{style:{flex:1}}),i.jsx("button",{type:"button","data-testid":`g7le-initstate-code-${t}`,onClick:()=>c(f=>!f),style:Hs,"aria-label":l("layout_editor.value_tree.show_source"),title:l("layout_editor.value_tree.show_source"),children:"</>"}),o?i.jsx("button",{type:"button","data-testid":`g7le-initstate-revert-${t}`,onClick:a,style:Hs,children:l("layout_editor.initstate.revert")}):i.jsx("button",{type:"button","data-testid":`g7le-initstate-remove-${t}`,onClick:a,style:Hs,"aria-label":l("layout_editor.init_actions.remove"),children:"✕"})]}),d?i.jsx("pre",{"data-testid":`g7le-initstate-code-block-${t}`,style:OS,children:p}):null,i.jsx(Nn,{value:n,onChange:s,t:l,path:t,scope:e}),r?i.jsxs("p",{"data-testid":`g7le-initstate-isolated-orphan-${t}`,style:IS,children:["⚠ ",l("layout_editor.initstate.isolated_orphan")]}):null]})}const yS={display:"flex",flexDirection:"column",gap:14,minWidth:0},bS={margin:0,fontSize:13,fontWeight:700,color:"#0f172a"},mS={margin:0,fontSize:11,color:"#1d4ed8",background:"#eff6ff",padding:"6px 8px",borderRadius:6},vS={display:"flex",flexDirection:"column",gap:8,padding:10,border:"1px solid #e2e8f0",borderRadius:10,minWidth:0},_S={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},xS={fontSize:12,fontWeight:700,color:"#475569"},wS={margin:0,fontSize:11,color:"#94a3b8"},kS={padding:"2px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},SS={display:"flex",flexDirection:"column",gap:4,padding:"6px 8px",border:"1px solid #f1f5f9",borderRadius:6,minWidth:0},CS={display:"flex",alignItems:"center",gap:6},$S={fontSize:12},ES={fontSize:12,color:"#0f172a",fontFamily:"monospace"},Hs={border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",fontSize:11,padding:"2px 6px"},IS={margin:0,fontSize:11,color:"#b45309",background:"#fef3c7",padding:"4px 6px",borderRadius:6},AS={display:"flex",gap:6,alignItems:"center"},TS={padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},DS={border:"1px solid #dc2626"},NS={margin:0,fontSize:11,color:"#dc2626"},zS={padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#0f172a"},MS={padding:"2px 10px",fontSize:11,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},LS={margin:0,fontSize:12,color:"#94a3b8"},PS={margin:0,fontSize:11,color:"#94a3b8"},OS={margin:"2px 0",padding:"6px 8px",fontSize:11,fontFamily:"monospace",color:"#0f172a",background:"#f1f5f9",border:"1px solid #e2e8f0",borderRadius:6,whiteSpace:"pre-wrap",wordBreak:"break-all",overflowX:"auto"},BS={self:"layout_editor.error_rows.source_self",inherited:"layout_editor.error_rows.source_inherited",template:"layout_editor.error_rows.source_template",none:"layout_editor.error_rows.source_none"};function Fs({value:e,onChange:t,t:n,codes:o,mode:r="local",sourceOf:s,renderActionList:a,testIdPrefix:l="g7le-error-rows"}){const d=o??Object.keys(e),c=u.useCallback((f,y)=>{t({...e,[f]:y})},[e,t]),p=u.useCallback(f=>{const y={...e};delete y[f],t(y)},[e,t]);return i.jsx("div",{className:l,"data-testid":l,children:d.map(f=>{const y=e[f],g=
Line truncated
|
||||
"page": "{{query.page ?? 1}}"
|
||||
}`,shapeErrorKey:"layout_editor.data_sources.error.params_object",invalidErrorKey:"layout_editor.data_sources.error.params_json",t:n,onChange:I=>x(P=>P&&{...P,paramsRawValue:I??{},paramsRawValid:!0}),onValidityChange:I=>x(P=>P&&{...P,paramsRawValid:I})}):i.jsxs("div",{"data-testid":"g7le-data-sources-field-params",children:[b.paramRows.length===0&&i.jsx("p",{"data-testid":"g7le-ds-params-empty",style:GC,children:n("layout_editor.data_sources.field.params_empty")}),i.jsx(zr,{value:b.paramRows.map(I=>({key:I.key,value:I.value})),onChange:I=>x({...b,paramRows:I.map(P=>({key:P.key??"",value:P.value??""}))}),keyField:"key",valueField:"value",t:n,candidates:d,testidPrefix:"g7le-ds-params-kv",keyPlaceholder:n("layout_editor.data_sources.field.param_key_placeholder"),valuePlaceholder:"{{query.page ?? 1}}",addLabel:n("layout_editor.data_sources.field.param_add")}),(()=>{const I=Ys(b.paramRows);return Object.keys(I).length===0?null:i.jsxs("div",{style:zu,children:[i.jsxs("button",{type:"button","data-testid":"g7le-ds-params-preview-toggle","aria-expanded":_,onClick:()=>C(P=>!P),style:Mu,children:[_?"▴":"▾"," ",n("layout_editor.data_sources.field.params_preview")]}),_&&i.jsx("pre",{"data-testid":"g7le-ds-params-preview",style:{...Lu,marginTop:4},children:JSON.stringify(I,null,2)})]})})()]}),i.jsx("div",{style:{fontSize:11,color:"#94a3b8",marginTop:4},children:n("layout_editor.data_sources.field.params_hint")})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.fallback")}),b.fallbackEnabled?i.jsxs("div",{"data-testid":"g7le-data-sources-field-fallback",style:{display:"flex",flexDirection:"column",gap:6},children:[i.jsx(Nn,{value:b.fallbackValue===void 0?{}:b.fallbackValue,onChange:I=>x({...b,fallbackValue:I}),t:n,path:"fallback",scope:"ds-fallback",renderStringLeaf:I=>i.jsx(Ve,{value:I.value,onChange:I.onChange,t:n,candidates:d,testidPrefix:I.testidPrefix})}),(()=>{const I=b.fallbackValue===void 0?{}:b.fallbackValue;return i.jsxs("div",{style:zu,children:[i.jsxs("button",{type:"button","data-testid":"g7le-ds-fallback-preview-toggle","aria-expanded":E,onClick:()=>S(P=>!P),style:Mu,children:[E?"▴":"▾"," ",n("layout_editor.data_sources.field.fallback_preview")]}),E&&i.jsx("pre",{"data-testid":"g7le-ds-fallback-preview",style:{...Lu,marginTop:4},children:JSON.stringify(I,null,2)})]})})(),i.jsx("button",{type:"button","data-testid":"g7le-data-sources-field-fallback-clear",onClick:()=>x({...b,fallbackEnabled:!1,fallbackValue:void 0}),style:{...rn,padding:"2px 8px",fontSize:11,alignSelf:"flex-start",color:"#dc2626",borderColor:"#fecaca"},children:n("layout_editor.data_sources.field.fallback_clear")})]}):i.jsxs("button",{type:"button","data-testid":"g7le-data-sources-field-fallback-add",onClick:()=>x({...b,fallbackEnabled:!0,fallbackValue:{}}),style:{...rn,padding:"4px 10px",display:"block"},children:["+ ",n("layout_editor.data_sources.field.fallback_add")]})]}),b.type==="websocket"&&i.jsxs("div",{"data-testid":"g7le-ds-ws-section",style:zo,children:[i.jsx("div",{style:Mo,children:n("layout_editor.data_sources.section.websocket")}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.channel")}),i.jsx("input",{"data-testid":"g7le-ds-ws-channel",style:jt,value:b.channel,onChange:I=>x({...b,channel:I.target.value}),placeholder:"core.admin.dashboard"})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.event")}),i.jsx("input",{"data-testid":"g7le-ds-ws-event",style:jt,value:b.event,onChange:I=>x({...b,event:I.target.value}),placeholder:"dashboard.stats.updated"})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.channel_type")}),i.jsx("select",{"data-testid":"g7le-ds-ws-channeltype",style:jt,value:b.channel_type,onChange:I=>x({...b,channel_type:I.target.value}),children:FC.map(I=>i.jsx("option",{value:I,children:I},I))})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.target_source")}),i.jsx("input",{"data-testid":"g7le-ds-ws-target",style:jt,value:b.target_source,onChange:I=>x({...b,target_source:I.target.value}),list:"g7le-ds-ws-target-list"}),i.jsx("datalist",{id:"g7le-ds-ws-target-list",children:h.filter(I=>I.id!==b.id).map(I=>typeof I.id=="string"?i.jsx("option",{value:I.id},I.id):null)})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.on_receive")}),i.jsx("div",{"data-testid":"g7le-ds-ws-onreceive",children:i.jsx(Gt,{actions:b.onReceive,onChange:I=>x({...b,onReceive:I}),t:n,recipes:s,candidatePools:z,chipContext:"payload",actionChipCandidates:l?.actionChipCandidates??null,testIdPrefix:"g7le-ds-onreceive-list"})})]})]}),i.jsxs("div",{style:zo,children:[i.jsx("div",{style:Mo,children:n("layout_editor.data_sources.section.on_success")}),i.jsx("div",{"data-testid":"g7le-ds-onsuccess",children:i.j
Line truncated
|
||||
`).split(`
|
||||
`);return t.length>0&&t[t.length-1]===""&&t.pop(),t}const Yu=4e3;function $$(e,t,n=3){const o=Gu(e),r=Gu(t);let s=0;const a=Math.min(o.length,r.length);for(;s<a&&o[s]===r[s];)s++;let l=0;const d=Math.min(o.length,r.length)-s;for(;l<d&&o[o.length-1-l]===r[r.length-1-l];)l++;const c=o.slice(s,o.length-l),p=r.slice(s,r.length-l);if(c.length>Yu||p.length>Yu)return{hunks:[],addedCount:-1,removedCount:-1,identical:!1};const f=S$(c,p),y=C$(c,p,f),g=[];let m=s,h=s,v=0,b=0;for(let w=0;w<s;w++)g.push({kind:"context",oldLine:w+1,newLine:w+1,content:o[w]});for(const w of y)w.kind==="context"?(m++,h++,g.push({kind:"context",oldLine:m,newLine:h,content:w.content})):w.kind==="remove"?(m++,b++,g.push({kind:"remove",oldLine:m,newLine:null,content:w.content})):(h++,v++,g.push({kind:"add",oldLine:null,newLine:h,content:w.content}));for(let w=0;w<l;w++){const _=o.length-l+w,C=r.length-l+w;g.push({kind:"context",oldLine:_+1,newLine:C+1,content:o[_]})}const x=v===0&&b===0;return{hunks:E$(g,n),addedCount:v,removedCount:b,identical:x}}function E$(e,t){const n=[];for(let r=0;r<e.length;r++)e[r].kind!=="context"&&n.push(r);if(n.length===0)return[];const o=[];for(const r of n){const s=Math.max(0,r-t),a=Math.min(e.length-1,r+t),l=o[o.length-1];l&&s<=l.end+1?l.end=Math.max(l.end,a):o.push({start:s,end:a})}return o.map(r=>{const s=e.slice(r.start,r.end+1);let a=0,l=0,d=0,c=0;for(const g of s)g.oldLine!==null&&(a===0&&(a=g.oldLine),d++),g.newLine!==null&&(l===0&&(l=g.newLine),c++);const p=a||(d===0?0:1),f=l||(c===0?0:1);return{header:`@@ -${p},${d} +${f},${c} @@`,oldStart:p,oldCount:d,newStart:f,newCount:c,lines:s}})}function Xu(e){return JSON.stringify(Js(e),null,2)}function Js(e){if(Array.isArray(e))return e.map(Js);if(e&&typeof e=="object"){const t=e,n={};for(const o of Object.keys(t).sort())n[o]=Js(t[o]);return n}return e}const I$={display:"flex",flexDirection:"column",height:"100%",minHeight:0},A$={display:"flex",alignItems:"center",gap:8,padding:"12px 16px",borderBottom:"1px solid #e2e8f0",background:"#f8fafc"},T$={display:"inline-flex",alignItems:"center",gap:4,padding:"4px 10px",borderRadius:6,border:"1px solid #cbd5e1",background:"#ffffff",color:"#0f172a",fontSize:12,fontWeight:600,cursor:"pointer",outline:"none"},D$={marginLeft:"auto",border:"none",background:"transparent",fontSize:16,color:"#64748b",cursor:"pointer",outline:"none"},N$={display:"flex",alignItems:"center",gap:12,padding:"8px 16px",borderBottom:"1px solid #e2e8f0",background:"#ffffff",fontSize:12,fontWeight:600},z$={flex:1,minHeight:0,overflow:"auto",background:"#ffffff",fontFamily:'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',fontSize:12,lineHeight:1.5},Ju={padding:"24px 12px",textAlign:"center",color:"#94a3b8",fontSize:13},M$={padding:"2px 12px",background:"#f1f5f9",color:"#64748b",borderTop:"1px solid #e2e8f0",borderBottom:"1px solid #e2e8f0",userSelect:"none"};function L$(e){return e==="add"?{row:{background:"#e6ffed"},gutter:{background:"#cdffd8",color:"#22863a"},sign:"+",signColor:"#22863a"}:e==="remove"?{row:{background:"#ffeef0"},gutter:{background:"#ffdce0",color:"#b31d28"},sign:"-",signColor:"#b31d28"}:{row:{background:"#ffffff"},gutter:{background:"#f8fafc",color:"#94a3b8"},sign:" ",signColor:"#94a3b8"}}const Zu={display:"inline-block",width:44,textAlign:"right",padding:"0 6px",color:"#94a3b8",userSelect:"none",borderRight:"1px solid #e2e8f0",whiteSpace:"nowrap"};function P$({oldVersion:e,newVersion:t,oldContent:n,newContent:o,t:r,onBack:s,onClose:a}){const l=u.useMemo(()=>{const c=Xu(n),p=Xu(o);return $$(c,p)},[n,o]),d=l.addedCount<0;return i.jsxs("div",{className:"g7le-version-diff","data-testid":"g7le-version-diff",style:I$,children:[i.jsxs("div",{"data-modal-drag-handle":!0,style:A$,children:[i.jsxs("button",{type:"button",onClick:s,style:T$,"data-testid":"g7le-version-diff-back",children:["← ",r("layout_editor.version_history.diff_back")]}),i.jsx("span",{style:{fontSize:14,fontWeight:700},"data-testid":"g7le-version-diff-title",children:r("layout_editor.version_history.diff_title",{old:e,new:t})}),i.jsx("button",{type:"button","aria-label":r("layout_editor.version_history.close"),onClick:a,style:D$,"data-testid":"g7le-version-diff-close",children:"✕"})]}),!d&&i.jsxs("div",{style:N$,"data-testid":"g7le-version-diff-summary",children:[i.jsx("span",{style:{color:"#16a34a"},"data-testid":"g7le-version-diff-added",children:r("layout_editor.version_history.added",{count:Math.max(0,l.addedCount)})}),i.jsx("span",{style:{color:"#dc2626"},"data-testid":"g7le-version-diff-removed",children:r("layout_editor.version_history.removed",{count:Math.max(0,l.removedCount)})})]}),i.jsx("div",{style:z$,children:d?i.jsx("div",{style:Ju,"data-testid":"g7le-version-diff-too-large",children:r("layout_editor.version_history.diff_too_large")}):l.identical?i.jsx("div",{style:Ju,"data-testid":"g7le-version-diff-identical",children:r("layout_editor.version_history.diff_identical")}):l.hunks.map((c,p)=>i.jsxs("div",{"data-testid":`gLine truncated
|
||||
`);return t.length>0&&t[t.length-1]===""&&t.pop(),t}const Yu=4e3;function $$(e,t,n=3){const o=Gu(e),r=Gu(t);let s=0;const a=Math.min(o.length,r.length);for(;s<a&&o[s]===r[s];)s++;let l=0;const d=Math.min(o.length,r.length)-s;for(;l<d&&o[o.length-1-l]===r[r.length-1-l];)l++;const c=o.slice(s,o.length-l),p=r.slice(s,r.length-l);if(c.length>Yu||p.length>Yu)return{hunks:[],addedCount:-1,removedCount:-1,identical:!1};const f=S$(c,p),y=C$(c,p,f),g=[];let m=s,h=s,v=0,b=0;for(let w=0;w<s;w++)g.push({kind:"context",oldLine:w+1,newLine:w+1,content:o[w]});for(const w of y)w.kind==="context"?(m++,h++,g.push({kind:"context",oldLine:m,newLine:h,content:w.content})):w.kind==="remove"?(m++,b++,g.push({kind:"remove",oldLine:m,newLine:null,content:w.content})):(h++,v++,g.push({kind:"add",oldLine:null,newLine:h,content:w.content}));for(let w=0;w<l;w++){const _=o.length-l+w,C=r.length-l+w;g.push({kind:"context",oldLine:_+1,newLine:C+1,content:o[_]})}const x=v===0&&b===0;return{hunks:E$(g,n),addedCount:v,removedCount:b,identical:x}}function E$(e,t){const n=[];for(let r=0;r<e.length;r++)e[r].kind!=="context"&&n.push(r);if(n.length===0)return[];const o=[];for(const r of n){const s=Math.max(0,r-t),a=Math.min(e.length-1,r+t),l=o[o.length-1];l&&s<=l.end+1?l.end=Math.max(l.end,a):o.push({start:s,end:a})}return o.map(r=>{const s=e.slice(r.start,r.end+1);let a=0,l=0,d=0,c=0;for(const g of s)g.oldLine!==null&&(a===0&&(a=g.oldLine),d++),g.newLine!==null&&(l===0&&(l=g.newLine),c++);const p=a||(d===0?0:1),f=l||(c===0?0:1);return{header:`@@ -${p},${d} +${f},${c} @@`,oldStart:p,oldCount:d,newStart:f,newCount:c,lines:s}})}function Xu(e){return JSON.stringify(Js(e),null,2)}function Js(e){if(Array.isArray(e))return e.map(Js);if(e&&typeof e=="object"){const t=e,n={};for(const o of Object.keys(t).sort())n[o]=Js(t[o]);return n}return e}const I$={display:"flex",flexDirection:"column",height:"100%",minHeight:0},A$={display:"flex",alignItems:"center",gap:8,padding:"12px 16px",borderBottom:"1px solid #e2e8f0",background:"#f8fafc"},T$={display:"inline-flex",alignItems:"center",gap:4,padding:"4px 10px",borderRadius:6,border:"1px solid #cbd5e1",background:"#ffffff",color:"#0f172a",fontSize:12,fontWeight:600,cursor:"pointer",outline:"none"},D$={marginLeft:"auto",border:"none",background:"transparent",fontSize:16,color:"#64748b",cursor:"pointer",outline:"none"},N$={display:"flex",alignItems:"center",gap:12,padding:"8px 16px",borderBottom:"1px solid #e2e8f0",background:"#ffffff",fontSize:12,fontWeight:600},z$={flex:1,minHeight:0,overflow:"auto",background:"#ffffff",fontFamily:'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',fontSize:12,lineHeight:1.5},Ju={padding:"24px 12px",textAlign:"center",color:"#94a3b8",fontSize:13},M$={padding:"2px 12px",background:"#f1f5f9",color:"#64748b",borderTop:"1px solid #e2e8f0",borderBottom:"1px solid #e2e8f0",userSelect:"none"};function L$(e){return e==="add"?{row:{background:"#e6ffed"},gutter:{background:"#cdffd8",color:"#22863a"},sign:"+",signColor:"#22863a"}:e==="remove"?{row:{background:"#ffeef0"},gutter:{background:"#ffdce0",color:"#b31d28"},sign:"-",signColor:"#b31d28"}:{row:{background:"#ffffff"},gutter:{background:"#f8fafc",color:"#94a3b8"},sign:" ",signColor:"#94a3b8"}}const Zu={display:"inline-block",width:44,textAlign:"right",padding:"0 6px",color:"#94a3b8",userSelect:"none",borderRight:"1px solid #e2e8f0",whiteSpace:"nowrap"};function P$({oldVersion:e,newVersion:t,oldContent:n,newContent:o,t:r,onBack:s,onClose:a}){const l=u.useMemo(()=>{const c=Xu(n),p=Xu(o);return $$(c,p)},[n,o]),d=l.addedCount<0;return i.jsxs("div",{className:"g7le-version-diff","data-testid":"g7le-version-diff",style:I$,children:[i.jsxs("div",{"data-modal-drag-handle":!0,style:A$,children:[i.jsxs("button",{type:"button",onClick:s,style:T$,"data-testid":"g7le-version-diff-back",children:["← ",r("layout_editor.version_history.diff_back")]}),i.jsx("span",{style:{fontSize:14,fontWeight:700},"data-testid":"g7le-version-diff-title",children:r("layout_editor.version_history.diff_title",{old:e,new:t})}),i.jsx("button",{type:"button","aria-label":r("layout_editor.version_history.close"),onClick:a,style:D$,"data-testid":"g7le-version-diff-close",children:"✕"})]}),!d&&i.jsxs("div",{style:N$,"data-testid":"g7le-version-diff-summary",children:[i.jsx("span",{style:{color:"#16a34a"},"data-testid":"g7le-version-diff-added",children:r("layout_editor.version_history.added",{count:Math.max(0,l.addedCount)})}),i.jsx("span",{style:{color:"#dc2626"},"data-testid":"g7le-version-diff-removed",children:r("layout_editor.version_history.removed",{count:Math.max(0,l.removedCount)})})]}),i.jsx("div",{style:z$,children:d?i.jsx("div",{style:Ju,"data-testid":"g7le-version-diff-too-large",children:r("layout_editor.version_history.diff_too_large")}):l.identical?i.jsx("div",{style:Ju,"data-testid":"g7le-version-diff-identical",children:r("layout_editor.version_history.diff_identical")}):l.hunks.map((c,p)=>i.jsxs("div",{"data-testid":`gLine truncated
|
||||
To pick up a draggable item, press the space bar.
|
||||
While dragging, use the arrow keys to move the item.
|
||||
Press space again to drop the item in its new position, or press escape to cancel.
|
||||
`},O7={onDragStart(e){let{active:t}=e;return"Picked up draggable item "+t.id+"."},onDragOver(e){let{active:t,over:n}=e;return n?"Draggable item "+t.id+" was moved over droppable area "+n.id+".":"Draggable item "+t.id+" is no longer over a droppable area."},onDragEnd(e){let{active:t,over:n}=e;return n?"Draggable item "+t.id+" was dropped over droppable area "+n.id:"Draggable item "+t.id+" was dropped."},onDragCancel(e){let{active:t}=e;return"Dragging was cancelled. Draggable item "+t.id+" was dropped."}};function B7(e){let{announcements:t=O7,container:n,hiddenTextDescribedById:o,screenReaderInstructions:r=P7}=e;const{announce:s,announcement:a}=z7(),l=ei("DndLiveRegion"),[d,c]=u.useState(!1);if(u.useEffect(()=>{c(!0)},[]),M7(u.useMemo(()=>({onDragStart(f){let{active:y}=f;s(t.onDragStart({active:y}))},onDragMove(f){let{active:y,over:g}=f;t.onDragMove&&s(t.onDragMove({active:y,over:g}))},onDragOver(f){let{active:y,over:g}=f;s(t.onDragOver({active:y,over:g}))},onDragEnd(f){let{active:y,over:g}=f;s(t.onDragEnd({active:y,over:g}))},onDragCancel(f){let{active:y,over:g}=f;s(t.onDragCancel({active:y,over:g}))}}),[s,t])),!d)return null;const p=u.createElement(u.Fragment,null,u.createElement(D7,{id:o,value:r.draggable}),u.createElement(N7,{id:l,announcement:a}));return n?Cn.createPortal(p,n):p}var it;(function(e){e.DragStart="dragStart",e.DragMove="dragMove",e.DragEnd="dragEnd",e.DragCancel="dragCancel",e.DragOver="dragOver",e.RegisterDroppable="registerDroppable",e.SetDroppableDisabled="setDroppableDisabled",e.UnregisterDroppable="unregisterDroppable"})(it||(it={}));function oi(){}function W7(e,t){return u.useMemo(()=>({sensor:e,options:t??{}}),[e,t])}function j7(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return u.useMemo(()=>[...t].filter(o=>o!=null),[...t])}const Ht=Object.freeze({x:0,y:0});function H7(e,t){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function F7(e,t){const n=ni(e);if(!n)return"0 0";const o={x:(n.x-t.left)/t.width*100,y:(n.y-t.top)/t.height*100};return o.x+"% "+o.y+"%"}function K7(e,t){let{data:{value:n}}=e,{data:{value:o}}=t;return n-o}function V7(e,t){let{data:{value:n}}=e,{data:{value:o}}=t;return o-n}function U7(e){let{left:t,top:n,height:o,width:r}=e;return[{x:t,y:n},{x:t+r,y:n},{x:t,y:n+o},{x:t+r,y:n+o}]}function q7(e,t){if(!e||e.length===0)return null;const[n]=e;return n[t]}function G7(e,t){const n=Math.max(t.top,e.top),o=Math.max(t.left,e.left),r=Math.min(t.left+t.width,e.left+e.width),s=Math.min(t.top+t.height,e.top+e.height),a=r-o,l=s-n;if(o<r&&n<s){const d=t.width*t.height,c=e.width*e.height,p=a*l,f=p/(d+c-p);return Number(f.toFixed(4))}return 0}const Y7=e=>{let{collisionRect:t,droppableRects:n,droppableContainers:o}=e;const r=[];for(const s of o){const{id:a}=s,l=n.get(a);if(l){const d=G7(l,t);d>0&&r.push({id:a,data:{droppableContainer:s,value:d}})}}return r.sort(V7)};function X7(e,t){const{top:n,left:o,bottom:r,right:s}=t;return n<=e.y&&e.y<=r&&o<=e.x&&e.x<=s}const J7=e=>{let{droppableContainers:t,droppableRects:n,pointerCoordinates:o}=e;if(!o)return[];const r=[];for(const s of t){const{id:a}=s,l=n.get(a);if(l&&X7(o,l)){const c=U7(l).reduce((f,y)=>f+H7(o,y),0),p=Number((c/4).toFixed(4));r.push({id:a,data:{droppableContainer:s,value:p}})}}return r.sort(K7)};function Z7(e,t,n){return{...e,scaleX:t&&n?t.width/n.width:1,scaleY:t&&n?t.height/n.height:1}}function Kp(e,t){return e&&t?{x:e.left-t.left,y:e.top-t.top}:Ht}function Q7(e){return function(n){for(var o=arguments.length,r=new Array(o>1?o-1:0),s=1;s<o;s++)r[s-1]=arguments[s];return r.reduce((a,l)=>({...a,top:a.top+e*l.y,bottom:a.bottom+e*l.y,left:a.left+e*l.x,right:a.right+e*l.x}),{...n})}}const R7=Q7(1);function Vp(e){if(e.startsWith("matrix3d(")){const t=e.slice(9,-1).split(/, /);return{x:+t[12],y:+t[13],scaleX:+t[0],scaleY:+t[5]}}else if(e.startsWith("matrix(")){const t=e.slice(7,-1).split(/, /);return{x:+t[4],y:+t[5],scaleX:+t[0],scaleY:+t[3]}}return null}function e2(e,t,n){const o=Vp(t);if(!o)return e;const{scaleX:r,scaleY:s,x:a,y:l}=o,d=e.left-a-(1-r)*parseFloat(n),c=e.top-l-(1-s)*parseFloat(n.slice(n.indexOf(" ")+1)),p=r?e.width/r:e.width,f=s?e.height/s:e.height;return{width:p,height:f,top:c,right:d+p,bottom:c+f,left:d}}const t2={ignoreTransform:!1};function Ho(e,t){t===void 0&&(t=t2);let n=e.getBoundingClientRect();if(t.ignoreTransform){const{transform:c,transformOrigin:p}=_t(e).getComputedStyle(e);c&&(n=e2(n,c,p))}const{top:o,left:r,width:s,height:a,bottom:l,right:d}=n;return{top:o,left:r,width:s,height:a,bottom:l,right:d}}function Up(e){return Ho(e,{ignoreTransform:!0})}function n2(e){const t=e.innerWidth,n=e.innerHeight;return{top:0,left:0,right:t,bottom:n,width:t,height:n}}function o2(e,t){return t===void 0&&(t=_t(e).getComputedStyle(e)),t.position==="fixed"}function r2(e,t){t===void 0&&(t=_t(e).getComputedStyle(e));const n=/(auto|scroll|overlay)/;return["overflow","overflowX","overflowY"].some(r=>{const s=t[r];return typeof s=="string"?n.test(s):!1})}function da(e,t){const n=[];
Line truncated
|
||||
`:"",e.message]})]}),i.jsxs("div",{style:{display:"flex",justifyContent:"center",gap:8,flexWrap:"wrap"},children:[e.kind==="unauthorized"&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-signin",onClick:a,style:Ka,children:t("layout_editor.access_error.action.sign_in")}),e.kind==="forbidden"&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-home",onClick:r,style:Ka,children:t("layout_editor.access_error.action.go_home")}),(e.kind==="not_found"||e.kind==="server_error"||e.kind==="network"||e.kind==="unknown")&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-retry",onClick:s,style:Ka,children:t("layout_editor.access_error.action.retry")}),e.kind!=="unauthorized"&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-home-secondary",onClick:r,style:S4,children:t("layout_editor.access_error.action.go_admin_home")})]})]})]})}const Ka={padding:"8px 16px",background:"#2563eb",color:"#fff",border:"1px solid #1d4ed8",borderRadius:6,fontSize:13,fontWeight:600,cursor:"pointer"},S4={padding:"8px 16px",background:"#fff",color:"#475569",border:"1px solid #cbd5e1",borderRadius:6,fontSize:13,fontWeight:500,cursor:"pointer"};function C4(e){const t=window;if(typeof t>"u")return{getSnapshot:()=>({globalState:{},localState:{}}),restore:()=>{}};const n=t.G7Core||{},o={templateApp:t.__templateApp,g7CoreState:n.state,g7CoreDispatch:n.dispatch,g7CoreData:n.data,g7CoreModal:n.modal,g7CoreToast:n.toast};let r={...e.initialGlobalState??{}};r._local||(r._local={});const s=[],a=()=>{const c={globalState:{...r},localState:{...r._local??{}}};for(const p of s)try{p(r)}catch{}e.onChange?.(c)},l={},d={getGlobalState:()=>r,setGlobalState:(c,p)=>{typeof c=="function"?r=c(r):r={...r,...c},a()},onGlobalStateChange:c=>(s.push(c),()=>{const p=s.indexOf(c);p>=0&&s.splice(p,1)}),get globalState(){return r},getActionDispatcher:()=>e.actionDispatcher,getDataSource:c=>l[c]??null,setDataSource:(c,p)=>{l[c]=p},refetchDataSource:async c=>null,updateDataSourceItem:(c,p,f)=>l[c]??null,getLocale:()=>e.locale,changeLocale:c=>{},getRouter:()=>({navigate:()=>{},replace:()=>{},push:()=>{}}),updateQueryParams:()=>{}};return t.__templateApp=d,n&&typeof n.dispatch=="function"&&(n.dispatch=(c,p)=>{try{return e.actionDispatcher.executeAction(c,p??{})}catch{return}}),n&&typeof n.modal=="object"&&n.modal!==null&&(n.modal={...n.modal,open:()=>{},close:()=>{},closeAll:()=>{}}),n&&typeof n.toast=="object"&&n.toast!==null&&(n.toast={...n.toast,push:()=>{},dismiss:()=>{}}),{getSnapshot:()=>({globalState:{...r},localState:{...r._local??{}}}),restore:()=>{t.__templateApp===d&&(t.__templateApp=o.templateApp),t.G7Core&&(o.g7CoreState!==void 0&&(t.G7Core.state=o.g7CoreState),o.g7CoreDispatch!==void 0&&(t.G7Core.dispatch=o.g7CoreDispatch),o.g7CoreData!==void 0&&(t.G7Core.data=o.g7CoreData),o.g7CoreModal!==void 0&&(t.G7Core.modal=o.g7CoreModal),o.g7CoreToast!==void 0&&(t.G7Core.toast=o.g7CoreToast)),s.length=0}}}const $4=new Set(["id","userId","postId","orderId","productId","categoryId","commentId","noticeId","inquiryId","addressId","wishlistId","notificationId","roleId","menuId"]);function E4(e){if(!e)return{};const t={},n=/:([a-zA-Z_][a-zA-Z0-9_]*)/g;let o;for(;(o=n.exec(e))!==null;){const r=o[1];t[r]=I4(r)}return t}function I4(e){return $4.has(e)||/Id$|_id$/.test(e)?"1":"sample"}const A4=bo("LayoutEditor");function Jo(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function bi(e){if(Array.isArray(e))return e.map(t=>bi(t));if(Jo(e)){const t={};for(const[n,o]of Object.entries(e))t[n]=bi(o);return t}return e}function mg(e,t="",n=new Set){for(const[o,r]of Object.entries(e)){const s=t?`${t}.${o}`:o;Jo(r)?mg(r,s,n):n.add(s)}return n}function vg(e,t,n,o,r,s=""){for(const[a,l]of Object.entries(t)){const d=s?`${s}.${a}`:a;if(n.has(d)){r(`[LayoutEditor] sampleGlobal key conflict: extension '${o}' override of core key '${d}' ignored`);continue}const c=e[a];Jo(l)&&Jo(c)?vg(c,l,n,o,r,d):e[a]=bi(l)}}function Va(e){const t=e.warn??(r=>A4.warn(r)),n=bi(e.coreSeed??{});e.isGuestOnly&&"currentUser"in n&&delete n.currentUser;const o=mg(n);for(const r of e.sources)Jo(r.sampleGlobal)&&vg(n,r.sampleGlobal,o,r.id,t);return e.isGuestOnly&&"currentUser"in n&&delete n.currentUser,n}function T4(e){const t={};if(!e||typeof e!="object"||Array.isArray(e))return t;for(const[n,o]of Object.entries(e))n.startsWith("$")&&(t[n]=o);return t}function D4(e){const t={};if(!e||typeof e!="object"||Array.isArray(e))return t;for(const[n,o]of Object.entries(e))n.startsWith("$")||(t[n]=o);return t}function N4(e){if(typeof e!="string")return null;const t=/^\s*\{\{\s*([\s\S]*?)\s*\}\}\s*$/.exec(e);if(!t)return null;let n=t[1];const o=n.indexOf("??");return o>=0&&(n=n.slice(0,o)),n=n.replace(/\?\./g,".").replace(/\s+/g,""),n===""||!/^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(n)?null:n.split(".")}function z4(e,t){const n=N4(t);if(!n||n.length===0)return e;let o=e;for(let a=0;a<n.length;a++){if(o==null||typeof o!="obj
Line truncated
|
||||
`},O7={onDragStart(e){let{active:t}=e;return"Picked up draggable item "+t.id+"."},onDragOver(e){let{active:t,over:n}=e;return n?"Draggable item "+t.id+" was moved over droppable area "+n.id+".":"Draggable item "+t.id+" is no longer over a droppable area."},onDragEnd(e){let{active:t,over:n}=e;return n?"Draggable item "+t.id+" was dropped over droppable area "+n.id:"Draggable item "+t.id+" was dropped."},onDragCancel(e){let{active:t}=e;return"Dragging was cancelled. Draggable item "+t.id+" was dropped."}};function B7(e){let{announcements:t=O7,container:n,hiddenTextDescribedById:o,screenReaderInstructions:r=P7}=e;const{announce:s,announcement:a}=z7(),l=ei("DndLiveRegion"),[d,c]=u.useState(!1);if(u.useEffect(()=>{c(!0)},[]),M7(u.useMemo(()=>({onDragStart(f){let{active:y}=f;s(t.onDragStart({active:y}))},onDragMove(f){let{active:y,over:g}=f;t.onDragMove&&s(t.onDragMove({active:y,over:g}))},onDragOver(f){let{active:y,over:g}=f;s(t.onDragOver({active:y,over:g}))},onDragEnd(f){let{active:y,over:g}=f;s(t.onDragEnd({active:y,over:g}))},onDragCancel(f){let{active:y,over:g}=f;s(t.onDragCancel({active:y,over:g}))}}),[s,t])),!d)return null;const p=u.createElement(u.Fragment,null,u.createElement(D7,{id:o,value:r.draggable}),u.createElement(N7,{id:l,announcement:a}));return n?Cn.createPortal(p,n):p}var it;(function(e){e.DragStart="dragStart",e.DragMove="dragMove",e.DragEnd="dragEnd",e.DragCancel="dragCancel",e.DragOver="dragOver",e.RegisterDroppable="registerDroppable",e.SetDroppableDisabled="setDroppableDisabled",e.UnregisterDroppable="unregisterDroppable"})(it||(it={}));function oi(){}function W7(e,t){return u.useMemo(()=>({sensor:e,options:t??{}}),[e,t])}function j7(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return u.useMemo(()=>[...t].filter(o=>o!=null),[...t])}const Ht=Object.freeze({x:0,y:0});function H7(e,t){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function F7(e,t){const n=ni(e);if(!n)return"0 0";const o={x:(n.x-t.left)/t.width*100,y:(n.y-t.top)/t.height*100};return o.x+"% "+o.y+"%"}function K7(e,t){let{data:{value:n}}=e,{data:{value:o}}=t;return n-o}function V7(e,t){let{data:{value:n}}=e,{data:{value:o}}=t;return o-n}function U7(e){let{left:t,top:n,height:o,width:r}=e;return[{x:t,y:n},{x:t+r,y:n},{x:t,y:n+o},{x:t+r,y:n+o}]}function q7(e,t){if(!e||e.length===0)return null;const[n]=e;return n[t]}function G7(e,t){const n=Math.max(t.top,e.top),o=Math.max(t.left,e.left),r=Math.min(t.left+t.width,e.left+e.width),s=Math.min(t.top+t.height,e.top+e.height),a=r-o,l=s-n;if(o<r&&n<s){const d=t.width*t.height,c=e.width*e.height,p=a*l,f=p/(d+c-p);return Number(f.toFixed(4))}return 0}const Y7=e=>{let{collisionRect:t,droppableRects:n,droppableContainers:o}=e;const r=[];for(const s of o){const{id:a}=s,l=n.get(a);if(l){const d=G7(l,t);d>0&&r.push({id:a,data:{droppableContainer:s,value:d}})}}return r.sort(V7)};function X7(e,t){const{top:n,left:o,bottom:r,right:s}=t;return n<=e.y&&e.y<=r&&o<=e.x&&e.x<=s}const J7=e=>{let{droppableContainers:t,droppableRects:n,pointerCoordinates:o}=e;if(!o)return[];const r=[];for(const s of t){const{id:a}=s,l=n.get(a);if(l&&X7(o,l)){const c=U7(l).reduce((f,y)=>f+H7(o,y),0),p=Number((c/4).toFixed(4));r.push({id:a,data:{droppableContainer:s,value:p}})}}return r.sort(K7)};function Z7(e,t,n){return{...e,scaleX:t&&n?t.width/n.width:1,scaleY:t&&n?t.height/n.height:1}}function Kp(e,t){return e&&t?{x:e.left-t.left,y:e.top-t.top}:Ht}function Q7(e){return function(n){for(var o=arguments.length,r=new Array(o>1?o-1:0),s=1;s<o;s++)r[s-1]=arguments[s];return r.reduce((a,l)=>({...a,top:a.top+e*l.y,bottom:a.bottom+e*l.y,left:a.left+e*l.x,right:a.right+e*l.x}),{...n})}}const R7=Q7(1);function Vp(e){if(e.startsWith("matrix3d(")){const t=e.slice(9,-1).split(/, /);return{x:+t[12],y:+t[13],scaleX:+t[0],scaleY:+t[5]}}else if(e.startsWith("matrix(")){const t=e.slice(7,-1).split(/, /);return{x:+t[4],y:+t[5],scaleX:+t[0],scaleY:+t[3]}}return null}function e2(e,t,n){const o=Vp(t);if(!o)return e;const{scaleX:r,scaleY:s,x:a,y:l}=o,d=e.left-a-(1-r)*parseFloat(n),c=e.top-l-(1-s)*parseFloat(n.slice(n.indexOf(" ")+1)),p=r?e.width/r:e.width,f=s?e.height/s:e.height;return{width:p,height:f,top:c,right:d+p,bottom:c+f,left:d}}const t2={ignoreTransform:!1};function Ho(e,t){t===void 0&&(t=t2);let n=e.getBoundingClientRect();if(t.ignoreTransform){const{transform:c,transformOrigin:p}=_t(e).getComputedStyle(e);c&&(n=e2(n,c,p))}const{top:o,left:r,width:s,height:a,bottom:l,right:d}=n;return{top:o,left:r,width:s,height:a,bottom:l,right:d}}function Up(e){return Ho(e,{ignoreTransform:!0})}function n2(e){const t=e.innerWidth,n=e.innerHeight;return{top:0,left:0,right:t,bottom:n,width:t,height:n}}function o2(e,t){return t===void 0&&(t=_t(e).getComputedStyle(e)),t.position==="fixed"}function r2(e,t){t===void 0&&(t=_t(e).getComputedStyle(e));const n=/(auto|scroll|overlay)/;return["overflow","overflowX","overflowY"].some(r=>{const s=t[r];return typeof s=="string"?n.test(s):!1})}function da(e,t){const n=[];
Line truncated
|
||||
`:"",e.message]})]}),i.jsxs("div",{style:{display:"flex",justifyContent:"center",gap:8,flexWrap:"wrap"},children:[e.kind==="unauthorized"&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-signin",onClick:a,style:Ka,children:t("layout_editor.access_error.action.sign_in")}),e.kind==="forbidden"&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-home",onClick:r,style:Ka,children:t("layout_editor.access_error.action.go_home")}),(e.kind==="not_found"||e.kind==="server_error"||e.kind==="network"||e.kind==="unknown")&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-retry",onClick:s,style:Ka,children:t("layout_editor.access_error.action.retry")}),e.kind!=="unauthorized"&&i.jsx("button",{type:"button","data-testid":"g7le-access-error-action-home-secondary",onClick:r,style:S4,children:t("layout_editor.access_error.action.go_admin_home")})]})]})]})}const Ka={padding:"8px 16px",background:"#2563eb",color:"#fff",border:"1px solid #1d4ed8",borderRadius:6,fontSize:13,fontWeight:600,cursor:"pointer"},S4={padding:"8px 16px",background:"#fff",color:"#475569",border:"1px solid #cbd5e1",borderRadius:6,fontSize:13,fontWeight:500,cursor:"pointer"};function C4(e){const t=window;if(typeof t>"u")return{getSnapshot:()=>({globalState:{},localState:{}}),restore:()=>{}};const n=t.G7Core||{},o={templateApp:t.__templateApp,g7CoreState:n.state,g7CoreDispatch:n.dispatch,g7CoreData:n.data,g7CoreModal:n.modal,g7CoreToast:n.toast};let r={...e.initialGlobalState??{}};r._local||(r._local={});const s=[],a=()=>{const c={globalState:{...r},localState:{...r._local??{}}};for(const p of s)try{p(r)}catch{}e.onChange?.(c)},l={},d={getGlobalState:()=>r,setGlobalState:(c,p)=>{typeof c=="function"?r=c(r):r={...r,...c},a()},onGlobalStateChange:c=>(s.push(c),()=>{const p=s.indexOf(c);p>=0&&s.splice(p,1)}),get globalState(){return r},getActionDispatcher:()=>e.actionDispatcher,getDataSource:c=>l[c]??null,setDataSource:(c,p)=>{l[c]=p},refetchDataSource:async c=>null,updateDataSourceItem:(c,p,f)=>l[c]??null,getLocale:()=>e.locale,changeLocale:c=>{},getRouter:()=>({navigate:()=>{},replace:()=>{},push:()=>{}}),updateQueryParams:()=>{}};return t.__templateApp=d,n&&typeof n.dispatch=="function"&&(n.dispatch=(c,p)=>{try{return e.actionDispatcher.executeAction(c,p??{})}catch{return}}),n&&typeof n.modal=="object"&&n.modal!==null&&(n.modal={...n.modal,open:()=>{},close:()=>{},closeAll:()=>{}}),n&&typeof n.toast=="object"&&n.toast!==null&&(n.toast={...n.toast,push:()=>{},dismiss:()=>{}}),{getSnapshot:()=>({globalState:{...r},localState:{...r._local??{}}}),restore:()=>{t.__templateApp===d&&(t.__templateApp=o.templateApp),t.G7Core&&(o.g7CoreState!==void 0&&(t.G7Core.state=o.g7CoreState),o.g7CoreDispatch!==void 0&&(t.G7Core.dispatch=o.g7CoreDispatch),o.g7CoreData!==void 0&&(t.G7Core.data=o.g7CoreData),o.g7CoreModal!==void 0&&(t.G7Core.modal=o.g7CoreModal),o.g7CoreToast!==void 0&&(t.G7Core.toast=o.g7CoreToast)),s.length=0}}}const $4=new Set(["id","userId","postId","orderId","productId","categoryId","commentId","noticeId","inquiryId","addressId","wishlistId","notificationId","roleId","menuId"]);function E4(e){if(!e)return{};const t={},n=/:([a-zA-Z_][a-zA-Z0-9_]*)/g;let o;for(;(o=n.exec(e))!==null;){const r=o[1];t[r]=I4(r)}return t}function I4(e){return $4.has(e)||/Id$|_id$/.test(e)?"1":"sample"}const A4=bo("LayoutEditor");function Jo(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function bi(e){if(Array.isArray(e))return e.map(t=>bi(t));if(Jo(e)){const t={};for(const[n,o]of Object.entries(e))t[n]=bi(o);return t}return e}function mg(e,t="",n=new Set){for(const[o,r]of Object.entries(e)){const s=t?`${t}.${o}`:o;Jo(r)?mg(r,s,n):n.add(s)}return n}function vg(e,t,n,o,r,s=""){for(const[a,l]of Object.entries(t)){const d=s?`${s}.${a}`:a;if(n.has(d)){r(`[LayoutEditor] sampleGlobal key conflict: extension '${o}' override of core key '${d}' ignored`);continue}const c=e[a];Jo(l)&&Jo(c)?vg(c,l,n,o,r,d):e[a]=bi(l)}}function Va(e){const t=e.warn??(r=>A4.warn(r)),n=bi(e.coreSeed??{});e.isGuestOnly&&"currentUser"in n&&delete n.currentUser;const o=mg(n);for(const r of e.sources)Jo(r.sampleGlobal)&&vg(n,r.sampleGlobal,o,r.id,t);return e.isGuestOnly&&"currentUser"in n&&delete n.currentUser,n}function T4(e){const t={};if(!e||typeof e!="object"||Array.isArray(e))return t;for(const[n,o]of Object.entries(e))n.startsWith("$")&&(t[n]=o);return t}function D4(e){const t={};if(!e||typeof e!="object"||Array.isArray(e))return t;for(const[n,o]of Object.entries(e))n.startsWith("$")||(t[n]=o);return t}function N4(e){if(typeof e!="string")return null;const t=/^\s*\{\{\s*([\s\S]*?)\s*\}\}\s*$/.exec(e);if(!t)return null;let n=t[1];const o=n.indexOf("??");return o>=0&&(n=n.slice(0,o)),n=n.replace(/\?\./g,".").replace(/\s+/g,""),n===""||!/^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(n)?null:n.split(".")}function z4(e,t){const n=N4(t);if(!n||n.length===0)return e;let o=e;for(let a=0;a<n.length;a++){if(o==null||typeof o!="obj
Line truncated
|
||||
//# sourceMappingURL=layout-editor.min.js.map
|
||||
+75
-32
@@ -1,20 +1,57 @@
|
||||
var lT=Object.defineProperty;var cT=(Yt,Xr,Jr)=>Xr in Yt?lT(Yt,Xr,{enumerable:!0,configurable:!0,writable:!0,value:Jr}):Yt[Xr]=Jr;var $=(Yt,Xr,Jr)=>cT(Yt,typeof Xr!="symbol"?Xr+"":Xr,Jr);(function(Yt){"use strict";function Xr(o,e){for(var n=0;n<e.length;n++){const a=e[n];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in o)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(o,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}))}function Jr(o){return o&&o.__esModule&&Object.prototype.hasOwnProperty.call(o,"default")?o.default:o}var Ju={exports:{}},We={};var qg;function PS(){if(qg)return We;qg=1;var o=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),l=Symbol.for("react.consumer"),c=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),f=Symbol.for("react.suspense"),g=Symbol.for("react.memo"),m=Symbol.for("react.lazy"),b=Symbol.for("react.activity"),S=Symbol.iterator;function v(H){return H===null||typeof H!="object"?null:(H=S&&H[S]||H["@@iterator"],typeof H=="function"?H:null)}var _={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},A=Object.assign,x={};function L(H,T,ce){this.props=H,this.context=T,this.refs=x,this.updater=ce||_}L.prototype.isReactComponent={},L.prototype.setState=function(H,T){if(typeof H!="object"&&typeof H!="function"&&H!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,H,T,"setState")},L.prototype.forceUpdate=function(H){this.updater.enqueueForceUpdate(this,H,"forceUpdate")};function M(){}M.prototype=L.prototype;function R(H,T,ce){this.props=H,this.context=T,this.refs=x,this.updater=ce||_}var D=R.prototype=new M;D.constructor=R,A(D,L.prototype),D.isPureReactComponent=!0;var B=Array.isArray;function q(){}var P={H:null,A:null,T:null,S:null},K=Object.prototype.hasOwnProperty;function ge(H,T,ce){var de=ce.ref;return{$$typeof:o,type:H,key:T,ref:de!==void 0?de:null,props:ce}}function Ee(H,T){return ge(H.type,T,H.props)}function Se(H){return typeof H=="object"&&H!==null&&H.$$typeof===o}function Ue(H){var T={"=":"=0",":":"=2"};return"$"+H.replace(/[=:]/g,function(ce){return T[ce]})}var Ve=/\/+/g;function qe(H,T){return typeof H=="object"&&H!==null&&H.key!=null?Ue(""+H.key):T.toString(36)}function St(H){switch(H.status){case"fulfilled":return H.value;case"rejected":throw H.reason;default:switch(typeof H.status=="string"?H.then(q,q):(H.status="pending",H.then(function(T){H.status==="pending"&&(H.status="fulfilled",H.value=T)},function(T){H.status==="pending"&&(H.status="rejected",H.reason=T)})),H.status){case"fulfilled":return H.value;case"rejected":throw H.reason}}throw H}function J(H,T,ce,de,ye){var ie=typeof H;(ie==="undefined"||ie==="boolean")&&(H=null);var xe=!1;if(H===null)xe=!0;else switch(ie){case"bigint":case"string":case"number":xe=!0;break;case"object":switch(H.$$typeof){case o:case e:xe=!0;break;case m:return xe=H._init,J(xe(H._payload),T,ce,de,ye)}}if(xe)return ye=ye(H),xe=de===""?"."+qe(H,0):de,B(ye)?(ce="",xe!=null&&(ce=xe.replace(Ve,"$&/")+"/"),J(ye,T,ce,"",function($e){return $e})):ye!=null&&(Se(ye)&&(ye=Ee(ye,ce+(ye.key==null||H&&H.key===ye.key?"":(""+ye.key).replace(Ve,"$&/")+"/")+xe)),T.push(ye)),1;xe=0;var _e=de===""?".":de+":";if(B(H))for(var Te=0;Te<H.length;Te++)de=H[Te],ie=_e+qe(de,Te),xe+=J(de,T,ce,ie,ye);else if(Te=v(H),typeof Te=="function")for(H=Te.call(H),Te=0;!(de=H.next()).done;)de=de.value,ie=_e+qe(de,Te++),xe+=J(de,T,ce,ie,ye);else if(ie==="object"){if(typeof H.then=="function")return J(St(H),T,ce,de,ye);throw T=String(H),Error("Objects are not valid as a React child (found: "+(T==="[object Object]"?"object with keys {"+Object.keys(H).join(", ")+"}":T)+"). If you meant to render a collection of children, use an array instead.")}return xe}function fe(H,T,ce){if(H==null)return H;var de=[],ye=0;return J(H,de,"","",function(ie){return T.call(ce,ie,ye++)}),de}function Le(H){if(H._status===-1){var T=H._result;T=T(),T.then(function(ce){(H._status===0||H._status===-1)&&(H._status=1,H._result=ce)},function(ce){(H._status===0||H._status===-1)&&(H._status=2,H._result=ce)}),H._status===-1&&(H._status=0,H._result=T)}if(H._status===1)return H._result.default;throw H._result}var Z=typeof reportError=="function"?reportError:function(H){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var T=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof H=="object"&&H!==null&&typeof H.message=="string"?String(H.message):String(H),error:H});if(!window.dispatchEvent(T))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",H);return}console.error(H)},pe={map:fe,forEach:function(H,T,Line truncated
|
||||
var qx=Object.defineProperty;var Gx=(Wt,Jr,Qr)=>Jr in Wt?qx(Wt,Jr,{enumerable:!0,configurable:!0,writable:!0,value:Qr}):Wt[Jr]=Qr;var $=(Wt,Jr,Qr)=>Gx(Wt,typeof Jr!="symbol"?Jr+"":Jr,Qr);(function(Wt){"use strict";function Jr(o,e){for(var r=0;r<e.length;r++){const a=e[r];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in o)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(o,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}))}function Qr(o){return o&&o.__esModule&&Object.prototype.hasOwnProperty.call(o,"default")?o.default:o}var Ju={exports:{}},Ke={};/**
|
||||
* @license React
|
||||
* react.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var Lg;function IS(){if(Lg)return Ke;Lg=1;var o=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),r=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),l=Symbol.for("react.consumer"),c=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),f=Symbol.for("react.suspense"),g=Symbol.for("react.memo"),m=Symbol.for("react.lazy"),y=Symbol.for("react.activity"),w=Symbol.iterator;function S(H){return H===null||typeof H!="object"?null:(H=w&&H[w]||H["@@iterator"],typeof H=="function"?H:null)}var v={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},A=Object.assign,x={};function R(H,T,ge){this.props=H,this.context=T,this.refs=x,this.updater=ge||v}R.prototype.isReactComponent={},R.prototype.setState=function(H,T){if(typeof H!="object"&&typeof H!="function"&&H!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,H,T,"setState")},R.prototype.forceUpdate=function(H){this.updater.enqueueForceUpdate(this,H,"forceUpdate")};function M(){}M.prototype=R.prototype;function O(H,T,ge){this.props=H,this.context=T,this.refs=x,this.updater=ge||v}var L=O.prototype=new M;L.constructor=O,A(L,R.prototype),L.isPureReactComponent=!0;var q=Array.isArray;function G(){}var B={H:null,A:null,T:null,S:null},K=Object.prototype.hasOwnProperty;function fe(H,T,ge){var de=ge.ref;return{$$typeof:o,type:H,key:T,ref:de!==void 0?de:null,props:ge}}function Ce(H,T){return fe(H.type,T,H.props)}function ke(H){return typeof H=="object"&&H!==null&&H.$$typeof===o}function Ue(H){var T={"=":"=0",":":"=2"};return"$"+H.replace(/[=:]/g,function(ge){return T[ge]})}var Pe=/\/+/g;function qe(H,T){return typeof H=="object"&&H!==null&&H.key!=null?Ue(""+H.key):T.toString(36)}function wt(H){switch(H.status){case"fulfilled":return H.value;case"rejected":throw H.reason;default:switch(typeof H.status=="string"?H.then(G,G):(H.status="pending",H.then(function(T){H.status==="pending"&&(H.status="fulfilled",H.value=T)},function(T){H.status==="pending"&&(H.status="rejected",H.reason=T)})),H.status){case"fulfilled":return H.value;case"rejected":throw H.reason}}throw H}function J(H,T,ge,de,me){var ae=typeof H;(ae==="undefined"||ae==="boolean")&&(H=null);var Re=!1;if(H===null)Re=!0;else switch(ae){case"bigint":case"string":case"number":Re=!0;break;case"object":switch(H.$$typeof){case o:case e:Re=!0;break;case m:return Re=H._init,J(Re(H._payload),T,ge,de,me)}}if(Re)return me=me(H),Re=de===""?"."+qe(H,0):de,q(me)?(ge="",Re!=null&&(ge=Re.replace(Pe,"$&/")+"/"),J(me,T,ge,"",function(Ge){return Ge})):me!=null&&(ke(me)&&(me=Ce(me,ge+(me.key==null||H&&H.key===me.key?"":(""+me.key).replace(Pe,"$&/")+"/")+Re)),T.push(me)),1;Re=0;var ze=de===""?".":de+":";if(q(H))for(var $e=0;$e<H.length;$e++)de=H[$e],ae=ze+qe(de,$e),Re+=J(de,T,ge,ae,me);else if($e=S(H),typeof $e=="function")for(H=$e.call(H),$e=0;!(de=H.next()).done;)de=de.value,ae=ze+qe(de,$e++),Re+=J(de,T,ge,ae,me);else if(ae==="object"){if(typeof H.then=="function")return J(wt(H),T,ge,de,me);throw T=String(H),Error("Objects are not valid as a React child (found: "+(T==="[object Object]"?"object with keys {"+Object.keys(H).join(", ")+"}":T)+"). If you meant to render a collection of children, use an array instead.")}return Re}function he(H,T,ge){if(H==null)return H;var de=[],me=0;return J(H,de,"","",function(ae){return T.call(ge,ae,me++)}),de}function Ee(H){if(H._status===-1){var T=H._result;T=T(),T.then(function(ge){(H._status===0||H._status===-1)&&(H._status=1,H._result=ge)},function(ge){(H._status===0||H._status===-1)&&(H._status=2,H._result=ge)}),H._status===-1&&(H._status=0,H._result=T)}if(H._status===1)return H._result.default;throw H._result}var Z=typeof reportError=="function"?reportError:function(H){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var T=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof H=="object"&&H!==null&&typeof H.message=="string"?String(H.message):String(H),error:H});if(!window.dispatchEvent(T))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",H);return}console.error(H)},pe={map:he,forEach:function(H,T,ge){he(H,function(){T.apply(this,arguments)},ge)},count:function(H){var T=0;return he(H,function(){T++}),T},toArray:function(H){return he(H,function(T){return T})||[]},only:function(H){if(!ke(H))throw Error("React.Children.only expected to receive a single React element child.");return H}};return Ke.Activity=y,Ke.Children=pe,Ke.Component=R,Ke.Fragment=r,Ke.Profiler=i,Ke.PureComponent=O,Ke.StrictMode=a,Ke.Suspense=f,Ke.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=B,Ke.__COMPILER_RUNTIME={__proto__:null,c:function(H){return B.H.useMemoCache(H)}},Ke.cache=function(H){return function(){return H.apply(null,arguments)}},Ke.cacheSignal=function(){return null},KLine truncated
|
||||
* @license React
|
||||
* scheduler.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var $g;function HS(){return $g||($g=1,(function(o){function e(J,he){var Ee=J.length;J.push(he);e:for(;0<Ee;){var Z=Ee-1>>>1,pe=J[Z];if(0<i(pe,he))J[Z]=he,J[Ee]=pe,Ee=Z;else break e}}function r(J){return J.length===0?null:J[0]}function a(J){if(J.length===0)return null;var he=J[0],Ee=J.pop();if(Ee!==he){J[0]=Ee;e:for(var Z=0,pe=J.length,H=pe>>>1;Z<H;){var T=2*(Z+1)-1,ge=J[T],de=T+1,me=J[de];if(0>i(ge,Ee))de<pe&&0>i(me,ge)?(J[Z]=me,J[de]=Ee,Z=de):(J[Z]=ge,J[T]=Ee,Z=T);else if(de<pe&&0>i(me,Ee))J[Z]=me,J[de]=Ee,Z=de;else break e}}return he}function i(J,he){var Ee=J.sortIndex-he.sortIndex;return Ee!==0?Ee:J.id-he.id}if(o.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var l=performance;o.unstable_now=function(){return l.now()}}else{var c=Date,d=c.now();o.unstable_now=function(){return c.now()-d}}var f=[],g=[],m=1,y=null,w=3,S=!1,v=!1,A=!1,x=!1,R=typeof setTimeout=="function"?setTimeout:null,M=typeof clearTimeout=="function"?clearTimeout:null,O=typeof setImmediate<"u"?setImmediate:null;function L(J){for(var he=r(g);he!==null;){if(he.callback===null)a(g);else if(he.startTime<=J)a(g),he.sortIndex=he.expirationTime,e(f,he);else break;he=r(g)}}function q(J){if(A=!1,L(J),!v)if(r(f)!==null)v=!0,G||(G=!0,Ue());else{var he=r(g);he!==null&&wt(q,he.startTime-J)}}var G=!1,B=-1,K=5,fe=-1;function Ce(){return x?!0:!(o.unstable_now()-fe<K)}function ke(){if(x=!1,G){var J=o.unstable_now();fe=J;var he=!0;try{e:{v=!1,A&&(A=!1,M(B),B=-1),S=!0;var Ee=w;try{t:{for(L(J),y=r(f);y!==null&&!(y.expirationTime>J&&Ce());){var Z=y.callback;if(typeof Z=="function"){y.callback=null,w=y.priorityLevel;var pe=Z(y.expirationTime<=J);if(J=o.unstable_now(),typeof pe=="function"){y.callback=pe,L(J),he=!0;break t}y===r(f)&&a(f),L(J)}else a(f);y=r(f)}if(y!==null)he=!0;else{var H=r(g);H!==null&&wt(q,H.startTime-J),he=!1}}break e}finally{y=null,w=Ee,S=!1}he=void 0}}finally{he?Ue():G=!1}}}var Ue;if(typeof O=="function")Ue=function(){O(ke)};else if(typeof MessageChannel<"u"){var Pe=new MessageChannel,qe=Pe.port2;Pe.port1.onmessage=ke,Ue=function(){qe.postMessage(null)}}else Ue=function(){R(ke,0)};function wt(J,he){B=R(function(){J(o.unstable_now())},he)}o.unstable_IdlePriority=5,o.unstable_ImmediatePriority=1,o.unstable_LowPriority=4,o.unstable_NormalPriority=3,o.unstable_Profiling=null,o.unstable_UserBlockingPriority=2,o.unstable_cancelCallback=function(J){J.callback=null},o.unstable_forceFrameRate=function(J){0>J||125<J?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):K=0<J?Math.floor(1e3/J):5},o.unstable_getCurrentPriorityLevel=function(){return w},o.unstable_next=function(J){switch(w){case 1:case 2:case 3:var he=3;break;default:he=w}var Ee=w;w=he;try{return J()}finally{w=Ee}},o.unstable_requestPaint=function(){x=!0},o.unstable_runWithPriority=function(J,he){switch(J){case 1:case 2:case 3:case 4:case 5:break;default:J=3}var Ee=w;w=J;try{return he()}finally{w=Ee}},o.unstable_scheduleCallback=function(J,he,Ee){var Z=o.unstable_now();switch(typeof Ee=="object"&&Ee!==null?(Ee=Ee.delay,Ee=typeof Ee=="number"&&0<Ee?Z+Ee:Z):Ee=Z,J){case 1:var pe=-1;break;case 2:pe=250;break;case 5:pe=1073741823;break;case 4:pe=1e4;break;default:pe=5e3}return pe=Ee+pe,J={id:m++,callback:he,priorityLevel:J,startTime:Ee,expirationTime:pe,sortIndex:-1},Ee>Z?(J.sortIndex=Ee,e(g,J),r(f)===null&&J===r(g)&&(A?(M(B),B=-1):A=!0,wt(q,Ee-Z))):(J.sortIndex=pe,e(f,J),v||S||(v=!0,G||(G=!0,Ue()))),J},o.unstable_shouldYield=Ce,o.unstable_wrapCallback=function(J){var he=w;return function(){var Ee=w;w=he;try{return J.apply(this,arguments)}finally{w=Ee}}}})(td)),td}var Ng;function jS(){return Ng||(Ng=1,ed.exports=HS()),ed.exports}var nd={exports:{}},vn={};/**
|
||||
* @license React
|
||||
* react-dom.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var Ig;function zS(){if(Ig)return vn;Ig=1;var o=Qu();function e(f){var g="https://react.dev/errors/"+f;if(1<arguments.length){g+="?args[]="+encodeURIComponent(arguments[1]);for(var m=2;m<arguments.length;m++)g+="&args[]="+encodeURIComponent(arguments[m])}return"Minified React error #"+f+"; visit "+g+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function r(){}var a={d:{f:r,r:function(){throw Error(e(522))},D:r,C:r,L:r,m:r,X:r,S:r,M:r},p:0,findDOMNode:null},i=Symbol.for("react.portal");function l(f,g,m){var y=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:i,key:y==null?null:""+y,children:f,containerInfo:g,implementation:m}}var c=o.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function d(f,g){if(f==="font")return"";if(typeof g=="string")return g==="use-credentials"?g:""}return vn.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=a,vn.createPortal=function(f,g){var m=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!g||g.nodeType!==1&&g.nodeType!==9&&g.nodeType!==11)throw Error(e(299));return l(f,g,null,m)},vn.flushSync=function(f){var g=c.T,m=a.p;try{if(c.T=null,a.p=2,f)return f()}finally{c.T=g,a.p=m,a.d.f()}},vn.preconnect=function(f,g){typeof f=="string"&&(g?(g=g.crossOrigin,g=typeof g=="string"?g==="use-credentials"?g:"":void 0):g=null,a.d.C(f,g))},vn.prefetchDNS=function(f){typeof f=="string"&&a.d.D(f)},vn.preinit=function(f,g){if(typeof f=="string"&&g&&typeof g.as=="string"){var m=g.as,y=d(m,g.crossOrigin),w=typeof g.integrity=="string"?g.integrity:void 0,S=typeof g.fetchPriority=="string"?g.fetchPriority:void 0;m==="style"?a.d.S(f,typeof g.precedence=="string"?g.precedence:void 0,{crossOrigin:y,integrity:w,fetchPriority:S}):m==="script"&&a.d.X(f,{crossOrigin:y,integrity:w,fetchPriority:S,nonce:typeof g.nonce=="string"?g.nonce:void 0})}},vn.preinitModule=function(f,g){if(typeof f=="string")if(typeof g=="object"&&g!==null){if(g.as==null||g.as==="script"){var m=d(g.as,g.crossOrigin);a.d.M(f,{crossOrigin:m,integrity:typeof g.integrity=="string"?g.integrity:void 0,nonce:typeof g.nonce=="string"?g.nonce:void 0})}}else g==null&&a.d.M(f)},vn.preload=function(f,g){if(typeof f=="string"&&typeof g=="object"&&g!==null&&typeof g.as=="string"){var m=g.as,y=d(m,g.crossOrigin);a.d.L(f,m,{crossOrigin:y,integrity:typeof g.integrity=="string"?g.integrity:void 0,nonce:typeof g.nonce=="string"?g.nonce:void 0,type:typeof g.type=="string"?g.type:void 0,fetchPriority:typeof g.fetchPriority=="string"?g.fetchPriority:void 0,referrerPolicy:typeof g.referrerPolicy=="string"?g.referrerPolicy:void 0,imageSrcSet:typeof g.imageSrcSet=="string"?g.imageSrcSet:void 0,imageSizes:typeof g.imageSizes=="string"?g.imageSizes:void 0,media:typeof g.media=="string"?g.media:void 0})}},vn.preloadModule=function(f,g){if(typeof f=="string")if(g){var m=d(g.as,g.crossOrigin);a.d.m(f,{as:typeof g.as=="string"&&g.as!=="script"?g.as:void 0,crossOrigin:m,integrity:typeof g.integrity=="string"?g.integrity:void 0})}else a.d.m(f)},vn.requestFormReset=function(f){a.d.r(f)},vn.unstable_batchedUpdates=function(f,g){return f(g)},vn.useFormState=function(f,g,m){return c.H.useFormState(f,g,m)},vn.useFormStatus=function(){return c.H.useHostTransitionStatus()},vn.version="19.2.0",vn}var Hg;function jg(){if(Hg)return nd.exports;Hg=1;function o(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(o)}catch(e){console.error(e)}}return o(),nd.exports=zS(),nd.exports}/**
|
||||
* @license React
|
||||
* react-dom-client.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var zg;function US(){if(zg)return Qs;zg=1;var o=jS(),e=Qu(),r=jg();function a(t){var n="https://react.dev/errors/"+t;if(1<arguments.length){n+="?args[]="+encodeURIComponent(arguments[1]);for(var s=2;s<arguments.length;s++)n+="&args[]="+encodeURIComponent(arguments[s])}return"Minified React error #"+t+"; visit "+n+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function i(t){return!(!t||t.nodeType!==1&&t.nodeType!==9&&t.nodeType!==11)}function l(t){var n=t,s=t;if(t.alternate)for(;n.return;)n=n.return;else{t=n;do n=t,(n.flags&4098)!==0&&(s=n.return),t=n.return;while(t)}return n.tag===3?s:null}function c(t){if(t.tag===13){var n=t.memoizedState;if(n===null&&(t=t.alternate,t!==null&&(n=t.memoizedState)),n!==null)return n.dehydrated}return null}function d(t){if(t.tag===31){var n=t.memoizedState;if(n===null&&(t=t.alternate,t!==null&&(n=t.memoizedState)),n!==null)return n.dehydrated}return null}function f(t){if(l(t)!==t)throw Error(a(188))}function g(t){var n=t.alternate;if(!n){if(n=l(t),n===null)throw Error(a(188));return n!==t?null:t}for(var s=t,u=n;;){var h=s.return;if(h===null)break;var p=h.alternate;if(p===null){if(u=h.return,u!==null){s=u;continue}break}if(h.child===p.child){for(p=h.child;p;){if(p===s)return f(h),t;if(p===u)return f(h),n;p=p.sibling}throw Error(a(188))}if(s.return!==u.return)s=h,u=p;else{for(var C=!1,k=h.child;k;){if(k===s){C=!0,s=h,u=p;break}if(k===u){C=!0,u=h,s=p;break}k=k.sibling}if(!C){for(k=p.child;k;){if(k===s){C=!0,s=p,u=h;break}if(k===u){C=!0,u=p,s=h;break}k=k.sibling}if(!C)throw Error(a(189))}}if(s.alternate!==u)throw Error(a(190))}if(s.tag!==3)throw Error(a(188));return s.stateNode.current===s?t:n}function m(t){var n=t.tag;if(n===5||n===26||n===27||n===6)return t;for(t=t.child;t!==null;){if(n=m(t),n!==null)return n;t=t.sibling}return null}var y=Object.assign,w=Symbol.for("react.element"),S=Symbol.for("react.transitional.element"),v=Symbol.for("react.portal"),A=Symbol.for("react.fragment"),x=Symbol.for("react.strict_mode"),R=Symbol.for("react.profiler"),M=Symbol.for("react.consumer"),O=Symbol.for("react.context"),L=Symbol.for("react.forward_ref"),q=Symbol.for("react.suspense"),G=Symbol.for("react.suspense_list"),B=Symbol.for("react.memo"),K=Symbol.for("react.lazy"),fe=Symbol.for("react.activity"),Ce=Symbol.for("react.memo_cache_sentinel"),ke=Symbol.iterator;function Ue(t){return t===null||typeof t!="object"?null:(t=ke&&t[ke]||t["@@iterator"],typeof t=="function"?t:null)}var Pe=Symbol.for("react.client.reference");function qe(t){if(t==null)return null;if(typeof t=="function")return t.$$typeof===Pe?null:t.displayName||t.name||null;if(typeof t=="string")return t;switch(t){case A:return"Fragment";case R:return"Profiler";case x:return"StrictMode";case q:return"Suspense";case G:return"SuspenseList";case fe:return"Activity"}if(typeof t=="object")switch(t.$$typeof){case v:return"Portal";case O:return t.displayName||"Context";case M:return(t._context.displayName||"Context")+".Consumer";case L:var n=t.render;return t=t.displayName,t||(t=n.displayName||n.name||"",t=t!==""?"ForwardRef("+t+")":"ForwardRef"),t;case B:return n=t.displayName||null,n!==null?n:qe(t.type)||"Memo";case K:n=t._payload,t=t._init;try{return qe(t(n))}catch{}}return null}var wt=Array.isArray,J=e.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,he=r.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,Ee={pending:!1,data:null,method:null,action:null},Z=[],pe=-1;function H(t){return{current:t}}function T(t){0>pe||(t.current=Z[pe],Z[pe]=null,pe--)}function ge(t,n){pe++,Z[pe]=t.current,t.current=n}var de=H(null),me=H(null),ae=H(null),Re=H(null);function ze(t,n){switch(ge(ae,n),ge(me,t),ge(de,null),n.nodeType){case 9:case 11:t=(t=n.documentElement)&&(t=t.namespaceURI)?rS(t):0;break;default:if(t=n.tagName,n=n.namespaceURI)n=rS(n),t=aS(n,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}T(de),ge(de,t)}function $e(){T(de),T(me),T(ae)}function Ge(t){t.memoizedState!==null&&ge(Re,t);var n=de.current,s=aS(n,t.type);n!==s&&(ge(me,t),ge(de,s))}function At(t){me.current===t&&(T(de),T(me)),Re.current===t&&(T(Re),Il._currentValue=Ee)}var yt,dn;function Tt(t){if(yt===void 0)try{throw Error()}catch(s){var n=s.stack.trim().match(/\n( *(at )?)/);yt=n&&n[1]||"",dn=-1<s.stack.indexOf(`
|
||||
at`)?" (<anonymous>)":-1<s.stack.indexOf("@")?"@unknown:0:0":""}return`
|
||||
`+wt+t+kt}var nn=!1;function Ft(t,r){if(!t||nn)return"";nn=!0;var s=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var u={DetermineComponentFrameRoot:function(){try{if(r){var le=function(){throw Error()};if(Object.defineProperty(le.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(le,[])}catch(te){var Q=te}Reflect.construct(t,[],le)}else{try{le.call()}catch(te){Q=te}t.call(le.prototype)}}else{try{throw Error()}catch(te){Q=te}(le=t())&&typeof le.catch=="function"&&le.catch(function(){})}}catch(te){if(te&&Q&&typeof te.stack=="string")return[te.stack,Q.stack]}return[null,null]}};u.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var h=Object.getOwnPropertyDescriptor(u.DetermineComponentFrameRoot,"name");h&&h.configurable&&Object.defineProperty(u.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var p=u.DetermineComponentFrameRoot(),w=p[0],k=p[1];if(w&&k){var j=w.split(`
|
||||
`),X=k.split(`
|
||||
`);for(h=u=0;u<j.length&&!j[u].includes("DetermineComponentFrameRoot");)u++;for(;h<X.length&&!X[h].includes("DetermineComponentFrameRoot");)h++;if(u===j.length||h===X.length)for(u=j.length-1,h=X.length-1;1<=u&&0<=h&&j[u]!==X[h];)h--;for(;1<=u&&0<=h;u--,h--)if(j[u]!==X[h]){if(u!==1||h!==1)do if(u--,h--,0>h||j[u]!==X[h]){var re=`
|
||||
`+j[u].replace(" at new "," at ");return t.displayName&&re.includes("<anonymous>")&&(re=re.replace("<anonymous>",t.displayName)),re}while(1<=u&&0<=h);break}}}finally{nn=!1,Error.prepareStackTrace=s}return(s=t?t.displayName||t.name:"")?ft(s):""}function Nt(t,r){switch(t.tag){case 26:case 27:case 5:return ft(t.type);case 16:return ft("Lazy");case 13:return t.child!==r&&r!==null?ft("Suspense Fallback"):ft("Suspense");case 19:return ft("SuspenseList");case 0:case 15:return Ft(t.type,!1);case 11:return Ft(t.type.render,!1);case 1:return Ft(t.type,!0);case 31:return ft("Activity");default:return""}}function Br(t){try{var r="",s=null;do r+=Nt(t,s),s=t,t=t.return;while(t);return r}catch(u){return`
|
||||
`+yt+t+dn}var Yt=!1;function ln(t,n){if(!t||Yt)return"";Yt=!0;var s=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var u={DetermineComponentFrameRoot:function(){try{if(n){var le=function(){throw Error()};if(Object.defineProperty(le.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(le,[])}catch(te){var Q=te}Reflect.construct(t,[],le)}else{try{le.call()}catch(te){Q=te}t.call(le.prototype)}}else{try{throw Error()}catch(te){Q=te}(le=t())&&typeof le.catch=="function"&&le.catch(function(){})}}catch(te){if(te&&Q&&typeof te.stack=="string")return[te.stack,Q.stack]}return[null,null]}};u.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var h=Object.getOwnPropertyDescriptor(u.DetermineComponentFrameRoot,"name");h&&h.configurable&&Object.defineProperty(u.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var p=u.DetermineComponentFrameRoot(),C=p[0],k=p[1];if(C&&k){var z=C.split(`
|
||||
`),Y=k.split(`
|
||||
`);for(h=u=0;u<z.length&&!z[u].includes("DetermineComponentFrameRoot");)u++;for(;h<Y.length&&!Y[h].includes("DetermineComponentFrameRoot");)h++;if(u===z.length||h===Y.length)for(u=z.length-1,h=Y.length-1;1<=u&&0<=h&&z[u]!==Y[h];)h--;for(;1<=u&&0<=h;u--,h--)if(z[u]!==Y[h]){if(u!==1||h!==1)do if(u--,h--,0>h||z[u]!==Y[h]){var ne=`
|
||||
`+z[u].replace(" at new "," at ");return t.displayName&&ne.includes("<anonymous>")&&(ne=ne.replace("<anonymous>",t.displayName)),ne}while(1<=u&&0<=h);break}}}finally{Yt=!1,Error.prepareStackTrace=s}return(s=t?t.displayName||t.name:"")?Tt(s):""}function zt(t,n){switch(t.tag){case 26:case 27:case 5:return Tt(t.type);case 16:return Tt("Lazy");case 13:return t.child!==n&&n!==null?Tt("Suspense Fallback"):Tt("Suspense");case 19:return Tt("SuspenseList");case 0:case 15:return ln(t.type,!1);case 11:return ln(t.type.render,!1);case 1:return ln(t.type,!0);case 31:return Tt("Activity");default:return""}}function Tr(t){try{var n="",s=null;do n+=zt(t,s),s=t,t=t.return;while(t);return n}catch(u){return`
|
||||
Error generating stack: `+u.message+`
|
||||
`+u.stack}}var fn=Object.prototype.hasOwnProperty,rn=o.unstable_scheduleCallback,hn=o.unstable_cancelCallback,Fn=o.unstable_shouldYield,Pr=o.unstable_requestPaint,Pt=o.unstable_now,wi=o.unstable_getCurrentPriorityLevel,na=o.unstable_ImmediatePriority,gn=o.unstable_UserBlockingPriority,Rn=o.unstable_NormalPriority,tt=o.unstable_LowPriority,$n=o.unstable_IdlePriority,pn=o.log,ra=o.unstable_setDisableYieldValue,ir=null,an=null;function En(t){if(typeof pn=="function"&&ra(t),an&&typeof an.setStrictMode=="function")try{an.setStrictMode(ir,t)}catch{}}var sn=Math.clz32?Math.clz32:ue,V=Math.log,se=Math.LN2;function ue(t){return t>>>=0,t===0?32:31-(V(t)/se|0)|0}var ee=256,ve=262144,he=4194304;function Ce(t){var r=t&42;if(r!==0)return r;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Ae(t,r,s){var u=t.pendingLanes;if(u===0)return 0;var h=0,p=t.suspendedLanes,w=t.pingedLanes;t=t.warmLanes;var k=u&134217727;return k!==0?(u=k&~p,u!==0?h=Ce(u):(w&=k,w!==0?h=Ce(w):s||(s=k&~t,s!==0&&(h=Ce(s))))):(k=u&~p,k!==0?h=Ce(k):w!==0?h=Ce(w):s||(s=u&~t,s!==0&&(h=Ce(s)))),h===0?0:r!==0&&r!==h&&(r&p)===0&&(p=h&-h,s=r&-r,p>=s||p===32&&(s&4194048)!==0)?r:h}function Fe(t,r){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&r)===0}function Re(t,r){switch(t){case 1:case 2:case 4:case 8:case 64:return r+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return r+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Ne(){var t=he;return he<<=1,(he&62914560)===0&&(he=4194304),t}function ke(t){for(var r=[],s=0;31>s;s++)r.push(t);return r}function Ze(t,r){t.pendingLanes|=r,r!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function ze(t,r,s,u,h,p){var w=t.pendingLanes;t.pendingLanes=s,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=s,t.entangledLanes&=s,t.errorRecoveryDisabledLanes&=s,t.shellSuspendCounter=0;var k=t.entanglements,j=t.expirationTimes,X=t.hiddenUpdates;for(s=w&~s;0<s;){var re=31-sn(s),le=1<<re;k[re]=0,j[re]=-1;var Q=X[re];if(Q!==null)for(X[re]=null,re=0;re<Q.length;re++){var te=Q[re];te!==null&&(te.lane&=-536870913)}s&=~le}u!==0&&Ge(t,u,0),p!==0&&h===0&&t.tag!==0&&(t.suspendedLanes|=p&~(w&~r))}function Ge(t,r,s){t.pendingLanes|=r,t.suspendedLanes&=~r;var u=31-sn(r);t.entangledLanes|=r,t.entanglements[u]=t.entanglements[u]|1073741824|s&261930}function we(t,r){var s=t.entangledLanes|=r;for(t=t.entanglements;s;){var u=31-sn(s),h=1<<u;h&r|t[u]&r&&(t[u]|=r),s&=~h}}function He(t,r){var s=r&-r;return s=(s&42)!==0?1:et(s),(s&(t.suspendedLanes|r))!==0?0:s}function et(t){switch(t){case 2:t=1;break;case 8:t=4;break;case 32:t=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:t=128;break;case 268435456:t=134217728;break;default:t=0}return t}function _t(t){return t&=-t,2<t?8<t?(t&134217727)!==0?32:268435456:8:2}function Lt(){var t=fe.p;return t!==0?t:(t=window.event,t===void 0?32:LS(t.type))}function kn(t,r){var s=fe.p;try{return fe.p=t,r()}finally{fe.p=s}}var It=Math.random().toString(36).slice(2),Ht="__reactFiber$"+It,_n="__reactProps$"+It,$a="__reactContainer$"+It,$o="__reactEvents$"+It,Af="__reactListeners$"+It,Ac="__reactHandles$"+It,qr="__reactResources$"+It,Ci="__reactMarker$"+It;function No(t){delete t[Ht],delete t[_n],delete t[$o],delete t[Af],delete t[Ac]}function Na(t){var r=t[Ht];if(r)return r;for(var s=t.parentNode;s;){if(r=s[$a]||s[Ht]){if(s=r.alternate,r.child!==null||s!==null&&s.child!==null)for(t=mS(t);t!==null;){if(s=t[Ht])return s;t=mS(t)}return r}t=s,s=t.parentNode}return null}function Gr(t){if(t=t[Ht]||t[$a]){var r=t.tag;if(r===5||r===6||r===13||r===31||r===26||r===27||r===3)return t}return null}function aa(t){var r=t.tag;if(r===5||r===26||r===27||r===6)return t.stateNode;throw Error(a(33))}function Ia(t){var r=t[qr];return r||(r=t[qr]={hoistableStyles:new Map,hoistableScripts:new Map}),r}function Xt(t){t[Ci]=!0}var xc=new Set,Tc={};function xr(t,r){Ha(t,r),Ha(t+"Capture",r)}function Ha(t,r){for(Tc[t]=r,t=0;t<r.length;t++)xc.add(r[t])}var xf=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u
Line truncated
|
||||
`).replace(Dx,"")}function sS(t,r){return r=iS(r),iS(t)===r}function xt(t,r,s,u,h,p){switch(s){case"children":typeof u=="string"?r==="body"||r==="textarea"&&u===""||Ba(t,u):(typeof u=="number"||typeof u=="bigint")&&r!=="body"&&Ba(t,""+u);break;case"className":Ei(t,"class",u);break;case"tabIndex":Ei(t,"tabindex",u);break;case"dir":case"role":case"viewBox":case"width":case"height":Ei(t,s,u);break;case"style":$c(t,u,p);break;case"data":if(r!=="object"){Ei(t,"data",u);break}case"src":case"href":if(u===""&&(r!=="a"||s!=="href")){t.removeAttribute(s);break}if(u==null||typeof u=="function"||typeof u=="symbol"||typeof u=="boolean"){t.removeAttribute(s);break}u=sr(""+u),t.setAttribute(s,u);break;case"action":case"formAction":if(typeof u=="function"){t.setAttribute(s,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof p=="function"&&(s==="formAction"?(r!=="input"&&xt(t,r,"name",h.name,h,null),xt(t,r,"formEncType",h.formEncType,h,null),xt(t,r,"formMethod",h.formMethod,h,null),xt(t,r,"formTarget",h.formTarget,h,null)):(xt(t,r,"encType",h.encType,h,null),xt(t,r,"method",h.method,h,null),xt(t,r,"target",h.target,h,null)));if(u==null||typeof u=="symbol"||typeof u=="boolean"){t.removeAttribute(s);break}u=sr(""+u),t.setAttribute(s,u);break;case"onClick":u!=null&&(t.onclick=Tr);break;case"onScroll":u!=null&&rt("scroll",t);break;case"onScrollEnd":u!=null&&rt("scrollend",t);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(a(61));if(s=u.__html,s!=null){if(h.children!=null)throw Error(a(60));t.innerHTML=s}}break;case"multiple":t.multiple=u&&typeof u!="function"&&typeof u!="symbol";break;case"muted":t.muted=u&&typeof u!="function"&&typeof u!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(u==null||typeof u=="function"||typeof u=="boolean"||typeof u=="symbol"){t.removeAttribute("xlink:href");break}s=sr(""+u),t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",s);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":u!=null&&typeof u!="function"&&typeof u!="symbol"?t.setAttribute(s,""+u):t.removeAttribute(s);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":u&&typeof u!="function"&&typeof u!="symbol"?t.setAttribute(s,""):t.removeAttribute(s);break;case"capture":case"download":u===!0?t.setAttribute(s,""):u!==!1&&u!=null&&typeof u!="function"&&typeof u!="symbol"?t.setAttribute(s,u):t.removeAttribute(s);break;case"cols":case"rows":case"size":case"span":u!=null&&typeof u!="function"&&typeof u!="symbol"&&!isNaN(u)&&1<=u?t.setAttribute(s,u):t.removeAttribute(s);break;case"rowSpan":case"start":u==null||typeof u=="function"||typeof u=="symbol"||isNaN(u)?t.removeAttribute(s):t.setAttribute(s,u);break;case"popover":rt("beforetoggle",t),rt("toggle",t),Nn(t,"popover",u);break;case"xlinkActuate":mn(t,"http://www.w3.org/1999/xlink","xlink:actuate",u);break;case"xlinkArcrole":mn(t,"http://www.w3.org/1999/xlink","xlink:arcrole",u);break;case"xlinkRole":mn(t,"http://www.w3.org/1999/xlink","xlink:role",u);break;case"xlinkShow":mn(t,"http://www.w3.org/1999/xlink","xlink:show",u);break;case"xlinkTitle":mn(t,"http://www.w3.org/1999/xlink","xlink:title",u);break;case"xlinkType":mn(t,"http://www.w3.org/1999/xlink","xlink:type",u);break;case"xmlBase":mn(t,"http://www.w3.org/XML/1998/namespace","xml:base",u);break;case"xmlLang":mn(t,"http://www.w3.org/XML/1998/namespace","xml:lang",u);break;case"xmlSpace":mn(t,"http://www.w3.org/XML/1998/namespace","xml:space",u);break;case"is":Nn(t,"is",u);break;case"innerText":case"textContent":break;default:(!(2<s.length)||s[0]!=="o"&&s[0]!=="O"||s[1]!=="n"&&s[1]!=="N")&&(s=Of.get(s)||s,Nn(t,s,u))}}function Ag(t,r,s,u,h,p){switch(s){case"style":$c(t,u,p);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(a(61));if(s=u.__html,s!=null){if(h.children!=null)throw Error(a(60));t.innerHTML=s}}break;case"children":typeof u=="string"?Ba(t,u):(typeof u=="number"||typeof u=="bigint")&&Ba(t,""+u);break;case"onScroll":u!=null&&rt("scroll",t);break;case"onScrollEnd":u!=null&&rt("scrollend",t);break;case"onClick":u!=null&&(t.onclick=Tr);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"inne
Line truncated
|
||||
`).forEach(function(c){i=c.indexOf(":"),n=c.substring(0,i).trim().toLowerCase(),a=c.substring(i+1).trim(),!(!n||e[n]&&a0[n])&&(n==="set-cookie"?e[n]?e[n].push(a):e[n]=[a]:e[n]=e[n]?e[n]+", "+a:a)}),e};function s0(o){let e=0,n=o.length;for(;e<n;){const a=o.charCodeAt(e);if(a!==9&&a!==32)break;e+=1}for(;n>e;){const a=o.charCodeAt(n-1);if(a!==9&&a!==32)break;n-=1}return e===0&&n===o.length?o:o.slice(e,n)}const o0=new RegExp("[\\u0000-\\u0008\\u000a-\\u001f\\u007f]+","g"),l0=new RegExp("[^\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+","g");function wd(o,e){return W.isArray(o)?o.map(n=>wd(n,e)):s0(String(o).replace(e,""))}const c0=o=>wd(o,o0),u0=o=>wd(o,l0);function Mp(o){const e=Object.create(null);return W.forEach(o.toJSON(),(n,a)=>{e[a]=u0(n)}),e}const $p=Symbol("internals");function co(o){return o&&String(o).trim().toLowerCase()}function Gl(o){return o===!1||o==null?o:W.isArray(o)?o.map(Gl):c0(String(o))}function d0(o){const e=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let a;for(;a=n.exec(o);)e[a[1]]=a[2];return e}const f0=o=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(o.trim());function Cd(o,e,n,a,i){if(W.isFunction(a))return a.call(this,e,n);if(i&&(e=n),!!W.isString(e)){if(W.isString(a))return e.indexOf(a)!==-1;if(W.isRegExp(a))return a.test(e)}}function h0(o){return o.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(e,n,a)=>n.toUpperCase()+a)}function g0(o,e){const n=W.toCamelCase(" "+e);["get","set","has"].forEach(a=>{Object.defineProperty(o,a+n,{__proto__:null,value:function(i,l,c){return this[a].call(this,e,i,l,c)},configurable:!0})})}let Tn=class{constructor(e){e&&this.set(e)}set(e,n,a){const i=this;function l(d,f,g){const m=co(f);if(!m)throw new Error("header name must be a non-empty string");const b=W.findKey(i,m);(!b||i[b]===void 0||g===!0||g===void 0&&i[b]!==!1)&&(i[b||f]=Gl(d))}const c=(d,f)=>W.forEach(d,(g,m)=>l(g,m,f));if(W.isPlainObject(e)||e instanceof this.constructor)c(e,n);else if(W.isString(e)&&(e=e.trim())&&!f0(e))c(i0(e),n);else if(W.isObject(e)&&W.isIterable(e)){let d={},f,g;for(const m of e){if(!W.isArray(m))throw TypeError("Object iterator must return a key-value pair");d[g=m[0]]=(f=d[g])?W.isArray(f)?[...f,m[1]]:[f,m[1]]:m[1]}c(d,n)}else e!=null&&l(n,e,a);return this}get(e,n){if(e=co(e),e){const a=W.findKey(this,e);if(a){const i=this[a];if(!n)return i;if(n===!0)return d0(i);if(W.isFunction(n))return n.call(this,i,a);if(W.isRegExp(n))return n.exec(i);throw new TypeError("parser must be boolean|regexp|function")}}}has(e,n){if(e=co(e),e){const a=W.findKey(this,e);return!!(a&&this[a]!==void 0&&(!n||Cd(this,this[a],a,n)))}return!1}delete(e,n){const a=this;let i=!1;function l(c){if(c=co(c),c){const d=W.findKey(a,c);d&&(!n||Cd(a,a[d],d,n))&&(delete a[d],i=!0)}}return W.isArray(e)?e.forEach(l):l(e),i}clear(e){const n=Object.keys(this);let a=n.length,i=!1;for(;a--;){const l=n[a];(!e||Cd(this,this[l],l,e,!0))&&(delete this[l],i=!0)}return i}normalize(e){const n=this,a={};return W.forEach(this,(i,l)=>{const c=W.findKey(a,l);if(c){n[c]=Gl(i),delete n[l];return}const d=e?h0(l):String(l).trim();d!==l&&delete n[l],n[d]=Gl(i),a[d]=!0}),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){const n=Object.create(null);return W.forEach(this,(a,i)=>{a!=null&&a!==!1&&(n[i]=e&&W.isArray(a)?a.join(", "):a)}),n}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([e,n])=>e+": "+n).join(`
|
||||
`)}getSetCookie(){return this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(e){return e instanceof this?e:new this(e)}static concat(e,...n){const a=new this(e);return n.forEach(i=>a.set(i)),a}static accessor(e){const a=(this[$p]=this[$p]={accessors:{}}).accessors,i=this.prototype;function l(c){const d=co(c);a[d]||(g0(i,c),a[d]=!0)}return W.isArray(e)?e.forEach(l):l(e),this}};Tn.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]),W.reduceDescriptors(Tn.prototype,({value:o},e)=>{let n=e[0].toUpperCase()+e.slice(1);return{get:()=>o,set(a){this[n]=a}}}),W.freezeMethods(Tn);const p0="[REDACTED ****]";function m0(o){if(W.hasOwnProp(o,"toJSON"))return!0;let e=Object.getPrototypeOf(o);for(;e&&e!==Object.prototype;){if(W.hasOwnProp(e,"toJSON"))return!0;e=Object.getPrototypeOf(e)}return!1}function y0(o,e){const n=new Set(e.map(l=>String(l).toLowerCase())),a=[],i=l=>{if(l===null||typeof l!="object"||W.isBuffer(l))return l;if(a.indexOf(l)!==-1)return;l instanceof Tn&&(l=l.toJSON()),a.push(l);let c;if(W.isArray(l))c=[],l.forEach((d,f)=>{const g=i(d);W.isUndefined(g)||(c[f]=g)});else{if(!W.isPlainObject(l)&&m0(l))return a.pop(),l;c=Object.create(null);for(const[d,f]of Object.entries(l)){const g=n.has(d.toLowerCase())?p0:i(f);W.isUndefined(g)||(c[d]=g)}}return a.pop(),c};return i(o)}let Oe=class jS extends Error{static from(e,n,a,i,l,c){const d=new jS(e.message,n||e.code,a,i,l);return d.cause=e,d.name=e.name,e.status!=null&&d.status==null&&(d.status=e.status),c&&Object.assign(d,c),d}constructor(e,n,a,i,l){super(e),Object.defineProperty(this,"message",{__proto__:null,value:e,enumerable:!0,writable:!0,configurable:!0}),this.name="AxiosError",this.isAxiosError=!0,n&&(this.code=n),a&&(this.config=a),i&&(this.request=i),l&&(this.response=l,this.status=l.status)}toJSON(){const e=this.config,n=e&&W.hasOwnProp(e,"redact")?e.redact:void 0,a=W.isArray(n)&&n.length>0?y0(e,n):W.toJSONObject(e);return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:a,code:this.code,status:this.status}}};Oe.ERR_BAD_OPTION_VALUE="ERR_BAD_OPTION_VALUE",Oe.ERR_BAD_OPTION="ERR_BAD_OPTION",Oe.ECONNABORTED="ECONNABORTED",Oe.ETIMEDOUT="ETIMEDOUT",Oe.ECONNREFUSED="ECONNREFUSED",Oe.ERR_NETWORK="ERR_NETWORK",Oe.ERR_FR_TOO_MANY_REDIRECTS="ERR_FR_TOO_MANY_REDIRECTS",Oe.ERR_DEPRECATED="ERR_DEPRECATED",Oe.ERR_BAD_RESPONSE="ERR_BAD_RESPONSE",Oe.ERR_BAD_REQUEST="ERR_BAD_REQUEST",Oe.ERR_CANCELED="ERR_CANCELED",Oe.ERR_NOT_SUPPORT="ERR_NOT_SUPPORT",Oe.ERR_INVALID_URL="ERR_INVALID_URL",Oe.ERR_FORM_DATA_DEPTH_EXCEEDED="ERR_FORM_DATA_DEPTH_EXCEEDED";const b0=null;function Ed(o){return W.isPlainObject(o)||W.isArray(o)}function Np(o){return W.endsWith(o,"[]")?o.slice(0,-2):o}function _d(o,e,n){return o?o.concat(e).map(function(i,l){return i=Np(i),!n&&l?"["+i+"]":i}).join(n?".":""):e}function v0(o){return W.isArray(o)&&!o.some(Ed)}const S0=W.toFlatObject(W,{},null,function(e){return/^is[A-Z]/.test(e)});function Vl(o,e,n){if(!W.isObject(o))throw new TypeError("target must be an object");e=e||new FormData,n=W.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(x,L){return!W.isUndefined(L[x])});const a=n.metaTokens,i=n.visitor||b,l=n.dots,c=n.indexes,d=n.Blob||typeof Blob<"u"&&Blob,f=n.maxDepth===void 0?100:n.maxDepth,g=d&&W.isSpecCompliantForm(e);if(!W.isFunction(i))throw new TypeError("visitor must be a function");function m(A){if(A===null)return"";if(W.isDate(A))return A.toISOString();if(W.isBoolean(A))return A.toString();if(!g&&W.isBlob(A))throw new Oe("Blob is not supported. Use a Buffer instead.");return W.isArrayBuffer(A)||W.isTypedArray(A)?g&&typeof Blob=="function"?new Blob([A]):Buffer.from(A):A}function b(A,x,L){let M=A;if(W.isReactNative(e)&&W.isReactNativeBlob(A))return e.append(_d(L,x,l),m(A)),!1;if(A&&!L&&typeof A=="object"){if(W.endsWith(x,"{}"))x=a?x:x.slice(0,-2),A=JSON.stringify(A);else if(W.isArray(A)&&v0(A)||(W.isFileList(A)||W.endsWith(x,"[]"))&&(M=W.toArray(A)))return x=Np(x),M.forEach(function(D,B){!(W.isUndefined(D)||D===null)&&e.append(c===!0?_d([x],B,l):c===null?x:x+"[]",m(D))}),!1}return Ed(A)?!0:(e.append(_d(L,x,l),m(A)),!1)}const S=[],v=Object.assign(S0,{defaultVisitor:b,convertValue:m,isVisitable:Ed});function _(A,x,L=0){if(!W.isUndefined(A)){if(L>f)throw new Oe("Object is too deeply nested ("+L+" levels). Max depth: "+f,Oe.ERR_FORM_DATA_DEPTH_EXCEEDED);if(S.indexOf(A)!==-1)throw Error("Circular reference detected in "+x.join("."));S.push(A),W.forEach(A,function(R,D){(!(W.isUndefined(R)||R===null)&&i.call(e,R,W.isString(D)?D.trim():D,x,v))===!0&&_(R,x?x.concat(D):[D],L+1)}),S.pop()}}if(!W.isObject(o))throw new TypeError("data must be an object");return _(o),e}function Ip(o){const e={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+"};return encodeURIComponent(o).replace(/[!'()~]|%20/g,function(a){return e[a]})}
Line truncated
|
||||
`+c.map(Qp).join(`
|
||||
`):" "+Qp(c[0]):"as no adapter specified";throw new Oe("There is no suitable adapter to dispatch the request "+d,"ERR_NOT_SUPPORT")}return i}const Zp={getAdapter:Y0,adapters:Od};function Ld(o){if(o.cancelToken&&o.cancelToken.throwIfRequested(),o.signal&&o.signal.aborted)throw new fo(null,o)}function em(o){return Ld(o),o.headers=Tn.from(o.headers),o.data=kd.call(o,o.transformRequest),["post","put","patch"].indexOf(o.method)!==-1&&o.headers.setContentType("application/x-www-form-urlencoded",!1),Zp.getAdapter(o.adapter||uo.adapter,o)(o).then(function(a){Ld(o),o.response=a;try{a.data=kd.call(o,o.transformResponse,a)}finally{delete o.response}return a.headers=Tn.from(a.headers),a},function(a){if(!Bp(a)&&(Ld(o),a&&a.response)){o.response=a.response;try{a.response.data=kd.call(o,o.transformResponse,a.response)}finally{delete o.response}a.response.headers=Tn.from(a.response.headers)}return Promise.reject(a)})}const Wl={};["object","boolean","number","function","string","symbol"].forEach((o,e)=>{Wl[o]=function(a){return typeof a===o||"a"+(e<1?"n ":" ")+o}});const tm={};Wl.transitional=function(e,n,a){function i(l,c){return"[Axios v"+Dd+"] Transitional option '"+l+"'"+c+(a?". "+a:"")}return(l,c,d)=>{if(e===!1)throw new Oe(i(c," has been removed"+(n?" in "+n:"")),Oe.ERR_DEPRECATED);return n&&!tm[c]&&(tm[c]=!0,console.warn(i(c," has been deprecated since v"+n+" and will be removed in the near future"))),e?e(l,c,d):!0}},Wl.spelling=function(e){return(n,a)=>(console.warn(`${a} is likely a misspelling of ${e}`),!0)};function X0(o,e,n){if(typeof o!="object")throw new Oe("options must be an object",Oe.ERR_BAD_OPTION_VALUE);const a=Object.keys(o);let i=a.length;for(;i-- >0;){const l=a[i],c=Object.prototype.hasOwnProperty.call(e,l)?e[l]:void 0;if(c){const d=o[l],f=d===void 0||c(d,l,o);if(f!==!0)throw new Oe("option "+l+" must be "+f,Oe.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new Oe("Unknown option "+l,Oe.ERR_BAD_OPTION)}}const Yl={assertOptions:X0,validators:Wl},nr=Yl.validators;let mi=class{constructor(e){this.defaults=e||{},this.interceptors={request:new jp,response:new jp}}async request(e,n){try{return await this._request(e,n)}catch(a){if(a instanceof Error){let i={};Error.captureStackTrace?Error.captureStackTrace(i):i=new Error;const l=(()=>{if(!i.stack)return"";const c=i.stack.indexOf(`
|
||||
`);return c===-1?"":i.stack.slice(c+1)})();try{if(!a.stack)a.stack=l;else if(l){const c=l.indexOf(`
|
||||
`),d=c===-1?-1:l.indexOf(`
|
||||
`,c+1),f=d===-1?"":l.slice(d+1);String(a.stack).endsWith(f)||(a.stack+=`
|
||||
`+l)}}catch{}}throw a}}_request(e,n){typeof e=="string"?(n=n||{},n.url=e):n=e||{},n=pi(this.defaults,n);const{transitional:a,paramsSerializer:i,headers:l}=n;a!==void 0&&Yl.assertOptions(a,{silentJSONParsing:nr.transitional(nr.boolean),forcedJSONParsing:nr.transitional(nr.boolean),clarifyTimeoutError:nr.transitional(nr.boolean),legacyInterceptorReqResOrdering:nr.transitional(nr.boolean)},!1),i!=null&&(W.isFunction(i)?n.paramsSerializer={serialize:i}:Yl.assertOptions(i,{encode:nr.function,serialize:nr.function},!0)),n.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?n.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:n.allowAbsoluteUrls=!0),Yl.assertOptions(n,{baseUrl:nr.spelling("baseURL"),withXsrfToken:nr.spelling("withXSRFToken")},!0),n.method=(n.method||this.defaults.method||"get").toLowerCase();let c=l&&W.merge(l.common,l[n.method]);l&&W.forEach(["delete","get","head","post","put","patch","query","common"],_=>{delete l[_]}),n.headers=Tn.concat(c,l);const d=[];let f=!0;this.interceptors.request.forEach(function(A){if(typeof A.runWhen=="function"&&A.runWhen(n)===!1)return;f=f&&A.synchronous;const x=n.transitional||xd;x&&x.legacyInterceptorReqResOrdering?d.unshift(A.fulfilled,A.rejected):d.push(A.fulfilled,A.rejected)});const g=[];this.interceptors.response.forEach(function(A){g.push(A.fulfilled,A.rejected)});let m,b=0,S;if(!f){const _=[em.bind(this),void 0];for(_.unshift(...d),_.push(...g),S=_.length,m=Promise.resolve(n);b<S;)m=m.then(_[b++],_[b++]);return m}S=d.length;let v=n;for(;b<S;){const _=d[b++],A=d[b++];try{v=_(v)}catch(x){A.call(this,x);break}}try{m=em.call(this,v)}catch(_){return Promise.reject(_)}for(b=0,S=g.length;b<S;)m=m.then(g[b++],g[b++]);return m}getUri(e){e=pi(this.defaults,e);const n=Vp(e.baseURL,e.url,e.allowAbsoluteUrls);return zp(n,e.params,e.paramsSerializer)}};W.forEach(["delete","get","head","options"],function(e){mi.prototype[e]=function(n,a){return this.request(pi(a||{},{method:e,url:n,data:(a||{}).data}))}}),W.forEach(["post","put","patch","query"],function(e){function n(a){return function(l,c,d){return this.request(pi(d||{},{method:e,headers:a?{"Content-Type":"multipart/form-data"}:{},url:l,data:c}))}}mi.prototype[e]=n(),e!=="query"&&(mi.prototype[e+"Form"]=n(!0))});let J0=class US{constructor(e){if(typeof e!="function")throw new TypeError("executor must be a function.");let n;this.promise=new Promise(function(l){n=l});const a=this;this.promise.then(i=>{if(!a._listeners)return;let l=a._listeners.length;for(;l-- >0;)a._listeners[l](i);a._listeners=null}),this.promise.then=i=>{let l;const c=new Promise(d=>{a.subscribe(d),l=d}).then(i);return c.cancel=function(){a.unsubscribe(l)},c},e(function(l,c,d){a.reason||(a.reason=new fo(l,c,d),n(a.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(e){if(this.reason){e(this.reason);return}this._listeners?this._listeners.push(e):this._listeners=[e]}unsubscribe(e){if(!this._listeners)return;const n=this._listeners.indexOf(e);n!==-1&&this._listeners.splice(n,1)}toAbortSignal(){const e=new AbortController,n=a=>{e.abort(a)};return this.subscribe(n),e.signal.unsubscribe=()=>this.unsubscribe(n),e.signal}static source(){let e;return{token:new US(function(i){e=i}),cancel:e}}};function Q0(o){return function(n){return o.apply(null,n)}}function Z0(o){return W.isObject(o)&&o.isAxiosError===!0}const Md={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(Md).forEach(([o,e])=>{Md[e]=o});function nm(o){const e=new mi(o),n=Ep(mi.prototype.request,e);return W.extend(n,mi.prototype,e,{allOwnKeys:!0}),W.extend(n,e,null,{allOwnKeys:!0}),n.create=function(i){return nm(pi(o,i))},n}const Vt=nm(uo);Vt.Axios=mi,Vt.CanceledError=fo,Vt.CancelToken=J0,Vt.isCa
Line truncated
|
||||
`+u.stack}}var wn=Object.prototype.hasOwnProperty,cn=o.unstable_scheduleCallback,fn=o.unstable_cancelCallback,sr=o.unstable_shouldYield,Wn=o.unstable_requestPaint,Ut=o.unstable_now,bt=o.unstable_getCurrentPriorityLevel,ra=o.unstable_ImmediatePriority,Ln=o.unstable_UserBlockingPriority,An=o.unstable_NormalPriority,xn=o.unstable_LowPriority,Mn=o.unstable_IdlePriority,hn=o.log,aa=o.unstable_setDisableYieldValue,$n=null,Vt=null;function Cn(t){if(typeof hn=="function"&&aa(t),Vt&&typeof Vt.setStrictMode=="function")try{Vt.setStrictMode($n,t)}catch{}}var nn=Math.clz32?Math.clz32:ce,V=Math.log,oe=Math.LN2;function ce(t){return t>>>=0,t===0?32:31-(V(t)/oe|0)|0}var X=256,ve=262144,ue=4194304;function we(t){var n=t&42;if(n!==0)return n;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function _e(t,n,s){var u=t.pendingLanes;if(u===0)return 0;var h=0,p=t.suspendedLanes,C=t.pingedLanes;t=t.warmLanes;var k=u&134217727;return k!==0?(u=k&~p,u!==0?h=we(u):(C&=k,C!==0?h=we(C):s||(s=k&~t,s!==0&&(h=we(s))))):(k=u&~p,k!==0?h=we(k):C!==0?h=we(C):s||(s=u&~t,s!==0&&(h=we(s)))),h===0?0:n!==0&&n!==h&&(n&p)===0&&(p=h&-h,s=n&-n,p>=s||p===32&&(s&4194048)!==0)?n:h}function Be(t,n){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&n)===0}function xe(t,n){switch(t){case 1:case 2:case 4:case 8:case 64:return n+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return n+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function De(){var t=ue;return ue<<=1,(ue&62914560)===0&&(ue=4194304),t}function Ae(t){for(var n=[],s=0;31>s;s++)n.push(t);return n}function et(t,n){t.pendingLanes|=n,n!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function Ne(t,n,s,u,h,p){var C=t.pendingLanes;t.pendingLanes=s,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=s,t.entangledLanes&=s,t.errorRecoveryDisabledLanes&=s,t.shellSuspendCounter=0;var k=t.entanglements,z=t.expirationTimes,Y=t.hiddenUpdates;for(s=C&~s;0<s;){var ne=31-nn(s),le=1<<ne;k[ne]=0,z[ne]=-1;var Q=Y[ne];if(Q!==null)for(Y[ne]=null,ne=0;ne<Q.length;ne++){var te=Q[ne];te!==null&&(te.lane&=-536870913)}s&=~le}u!==0&&je(t,u,0),p!==0&&h===0&&t.tag!==0&&(t.suspendedLanes|=p&~(C&~n))}function je(t,n,s){t.pendingLanes|=n,t.suspendedLanes&=~n;var u=31-nn(n);t.entangledLanes|=n,t.entanglements[u]=t.entanglements[u]|1073741824|s&261930}function Se(t,n){var s=t.entangledLanes|=n;for(t=t.entanglements;s;){var u=31-nn(s),h=1<<u;h&n|t[u]&n&&(t[u]|=n),s&=~h}}function Me(t,n){var s=n&-n;return s=(s&42)!==0?1:Ze(s),(s&(t.suspendedLanes|n))!==0?0:s}function Ze(t){switch(t){case 2:t=1;break;case 8:t=4;break;case 32:t=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:t=128;break;case 268435456:t=134217728;break;default:t=0}return t}function kt(t){return t&=-t,2<t?8<t?(t&134217727)!==0?32:268435456:8:2}function Ot(){var t=he.p;return t!==0?t:(t=window.event,t===void 0?32:TS(t.type))}function Nn(t,n){var s=he.p;try{return he.p=t,n()}finally{he.p=s}}var $t=Math.random().toString(36).slice(2),Nt="__reactFiber$"+$t,gn="__reactProps$"+$t,In="__reactContainer$"+$t,No="__reactEvents$"+$t,Sf="__reactListeners$"+$t,wf="__reactHandles$"+$t,hs="__reactResources$"+$t,ia="__reactMarker$"+$t;function Io(t){delete t[Nt],delete t[gn],delete t[No],delete t[Sf],delete t[wf]}function Ia(t){var n=t[Nt];if(n)return n;for(var s=t.parentNode;s;){if(n=s[In]||s[Nt]){if(s=n.alternate,n.child!==null||s!==null&&s.child!==null)for(t=dS(t);t!==null;){if(s=t[Nt])return s;t=dS(t)}return n}t=s,s=t.parentNode}return null}function Ha(t){if(t=t[Nt]||t[In]){var n=t.tag;if(n===5||n===6||n===13||n===31||n===26||n===27||n===3)return t}return null}function _i(t){var n=t.tag;if(n===5||n===26||n===27||n===6)return t.stateNode;throw Error(a(33))}function qr(t){var n=t[hs];return n||(n=t[hs]={hoistableStyles:new Map,hoistableScripts:new Map}),n}function Xt(t){t[ia]=!0}var Mc=new Set,$c={};function sa(t,n){ja(t,n),ja(t+"Capture",n)}function ja(t,n){for($c[t]=n,t=0;t<n.length;t++)Mc.add(n[t])}var kr=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u2
Line truncated
|
||||
`).replace(dx,"")}function tS(t,n){return n=eS(n),eS(t)===n}function Et(t,n,s,u,h,p){switch(s){case"children":typeof u=="string"?n==="body"||n==="textarea"&&u===""||Ua(t,u):(typeof u=="number"||typeof u=="bigint")&&n!=="body"&&Ua(t,""+u);break;case"className":ps(t,"class",u);break;case"tabIndex":ps(t,"tabindex",u);break;case"dir":case"role":case"viewBox":case"width":case"height":ps(t,s,u);break;case"style":Vr(t,u,p);break;case"data":if(n!=="object"){ps(t,"data",u);break}case"src":case"href":if(u===""&&(n!=="a"||s!=="href")){t.removeAttribute(s);break}if(u==null||typeof u=="function"||typeof u=="symbol"||typeof u=="boolean"){t.removeAttribute(s);break}u=bs(""+u),t.setAttribute(s,u);break;case"action":case"formAction":if(typeof u=="function"){t.setAttribute(s,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof p=="function"&&(s==="formAction"?(n!=="input"&&Et(t,n,"name",h.name,h,null),Et(t,n,"formEncType",h.formEncType,h,null),Et(t,n,"formMethod",h.formMethod,h,null),Et(t,n,"formTarget",h.formTarget,h,null)):(Et(t,n,"encType",h.encType,h,null),Et(t,n,"method",h.method,h,null),Et(t,n,"target",h.target,h,null)));if(u==null||typeof u=="symbol"||typeof u=="boolean"){t.removeAttribute(s);break}u=bs(""+u),t.setAttribute(s,u);break;case"onClick":u!=null&&(t.onclick=or);break;case"onScroll":u!=null&&nt("scroll",t);break;case"onScrollEnd":u!=null&&nt("scrollend",t);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(a(61));if(s=u.__html,s!=null){if(h.children!=null)throw Error(a(60));t.innerHTML=s}}break;case"multiple":t.multiple=u&&typeof u!="function"&&typeof u!="symbol";break;case"muted":t.muted=u&&typeof u!="function"&&typeof u!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(u==null||typeof u=="function"||typeof u=="boolean"||typeof u=="symbol"){t.removeAttribute("xlink:href");break}s=bs(""+u),t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",s);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":u!=null&&typeof u!="function"&&typeof u!="symbol"?t.setAttribute(s,""+u):t.removeAttribute(s);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":u&&typeof u!="function"&&typeof u!="symbol"?t.setAttribute(s,""):t.removeAttribute(s);break;case"capture":case"download":u===!0?t.setAttribute(s,""):u!==!1&&u!=null&&typeof u!="function"&&typeof u!="symbol"?t.setAttribute(s,u):t.removeAttribute(s);break;case"cols":case"rows":case"size":case"span":u!=null&&typeof u!="function"&&typeof u!="symbol"&&!isNaN(u)&&1<=u?t.setAttribute(s,u):t.removeAttribute(s);break;case"rowSpan":case"start":u==null||typeof u=="function"||typeof u=="symbol"||isNaN(u)?t.removeAttribute(s):t.setAttribute(s,u);break;case"popover":nt("beforetoggle",t),nt("toggle",t),gs(t,"popover",u);break;case"xlinkActuate":Rr(t,"http://www.w3.org/1999/xlink","xlink:actuate",u);break;case"xlinkArcrole":Rr(t,"http://www.w3.org/1999/xlink","xlink:arcrole",u);break;case"xlinkRole":Rr(t,"http://www.w3.org/1999/xlink","xlink:role",u);break;case"xlinkShow":Rr(t,"http://www.w3.org/1999/xlink","xlink:show",u);break;case"xlinkTitle":Rr(t,"http://www.w3.org/1999/xlink","xlink:title",u);break;case"xlinkType":Rr(t,"http://www.w3.org/1999/xlink","xlink:type",u);break;case"xmlBase":Rr(t,"http://www.w3.org/XML/1998/namespace","xml:base",u);break;case"xmlLang":Rr(t,"http://www.w3.org/XML/1998/namespace","xml:lang",u);break;case"xmlSpace":Rr(t,"http://www.w3.org/XML/1998/namespace","xml:space",u);break;case"is":gs(t,"is",u);break;case"innerText":case"textContent":break;default:(!(2<s.length)||s[0]!=="o"&&s[0]!=="O"||s[1]!=="n"&&s[1]!=="N")&&(s=_f.get(s)||s,gs(t,s,u))}}function gg(t,n,s,u,h,p){switch(s){case"style":Vr(t,u,p);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(a(61));if(s=u.__html,s!=null){if(h.children!=null)throw Error(a(60));t.innerHTML=s}}break;case"children":typeof u=="string"?Ua(t,u):(typeof u=="number"||typeof u=="bigint")&&Ua(t,""+u);break;case"onScroll":u!=null&&nt("scroll",t);break;case"onScrollEnd":u!=null&&nt("scrollend",t);break;case"onClick":u!=null&&(t.onclick=or);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"inne
Line truncated
|
||||
`).forEach(function(c){i=c.indexOf(":"),r=c.substring(0,i).trim().toLowerCase(),a=c.substring(i+1).trim(),!(!r||e[r]&&s0[r])&&(r==="set-cookie"?e[r]?e[r].push(a):e[r]=[a]:e[r]=e[r]?e[r]+", "+a:a)}),e},Op=Symbol("internals");function ho(o){return o&&String(o).trim().toLowerCase()}function Zl(o){return o===!1||o==null?o:ee.isArray(o)?o.map(Zl):String(o)}function l0(o){const e=Object.create(null),r=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let a;for(;a=r.exec(o);)e[a[1]]=a[2];return e}const c0=o=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(o.trim());function _d(o,e,r,a,i){if(ee.isFunction(a))return a.call(this,e,r);if(i&&(e=r),!!ee.isString(e)){if(ee.isString(a))return e.indexOf(a)!==-1;if(ee.isRegExp(a))return a.test(e)}}function u0(o){return o.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(e,r,a)=>r.toUpperCase()+a)}function d0(o,e){const r=ee.toCamelCase(" "+e);["get","set","has"].forEach(a=>{Object.defineProperty(o,a+r,{value:function(i,l,c){return this[a].call(this,e,i,l,c)},configurable:!0})})}let Rn=class{constructor(e){e&&this.set(e)}set(e,r,a){const i=this;function l(d,f,g){const m=ho(f);if(!m)throw new Error("header name must be a non-empty string");const y=ee.findKey(i,m);(!y||i[y]===void 0||g===!0||g===void 0&&i[y]!==!1)&&(i[y||f]=Zl(d))}const c=(d,f)=>ee.forEach(d,(g,m)=>l(g,m,f));if(ee.isPlainObject(e)||e instanceof this.constructor)c(e,r);else if(ee.isString(e)&&(e=e.trim())&&!c0(e))c(o0(e),r);else if(ee.isObject(e)&&ee.isIterable(e)){let d={},f,g;for(const m of e){if(!ee.isArray(m))throw TypeError("Object iterator must return a key-value pair");d[g=m[0]]=(f=d[g])?ee.isArray(f)?[...f,m[1]]:[f,m[1]]:m[1]}c(d,r)}else e!=null&&l(r,e,a);return this}get(e,r){if(e=ho(e),e){const a=ee.findKey(this,e);if(a){const i=this[a];if(!r)return i;if(r===!0)return l0(i);if(ee.isFunction(r))return r.call(this,i,a);if(ee.isRegExp(r))return r.exec(i);throw new TypeError("parser must be boolean|regexp|function")}}}has(e,r){if(e=ho(e),e){const a=ee.findKey(this,e);return!!(a&&this[a]!==void 0&&(!r||_d(this,this[a],a,r)))}return!1}delete(e,r){const a=this;let i=!1;function l(c){if(c=ho(c),c){const d=ee.findKey(a,c);d&&(!r||_d(a,a[d],d,r))&&(delete a[d],i=!0)}}return ee.isArray(e)?e.forEach(l):l(e),i}clear(e){const r=Object.keys(this);let a=r.length,i=!1;for(;a--;){const l=r[a];(!e||_d(this,this[l],l,e,!0))&&(delete this[l],i=!0)}return i}normalize(e){const r=this,a={};return ee.forEach(this,(i,l)=>{const c=ee.findKey(a,l);if(c){r[c]=Zl(i),delete r[l];return}const d=e?u0(l):String(l).trim();d!==l&&delete r[l],r[d]=Zl(i),a[d]=!0}),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){const r=Object.create(null);return ee.forEach(this,(a,i)=>{a!=null&&a!==!1&&(r[i]=e&&ee.isArray(a)?a.join(", "):a)}),r}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([e,r])=>e+": "+r).join(`
|
||||
`)}getSetCookie(){return this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(e){return e instanceof this?e:new this(e)}static concat(e,...r){const a=new this(e);return r.forEach(i=>a.set(i)),a}static accessor(e){const a=(this[Op]=this[Op]={accessors:{}}).accessors,i=this.prototype;function l(c){const d=ho(c);a[d]||(d0(i,c),a[d]=!0)}return ee.isArray(e)?e.forEach(l):l(e),this}};Rn.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]),ee.reduceDescriptors(Rn.prototype,({value:o},e)=>{let r=e[0].toUpperCase()+e.slice(1);return{get:()=>o,set(a){this[r]=a}}}),ee.freezeMethods(Rn);function Ad(o,e){const r=this||fo,a=e||r,i=Rn.from(a.headers);let l=a.data;return ee.forEach(o,function(d){l=d.call(r,l,i.normalize(),e?e.status:void 0)}),i.normalize(),l}function Lp(o){return!!(o&&o.__CANCEL__)}function ns(o,e,r){Ye.call(this,o??"canceled",Ye.ERR_CANCELED,e,r),this.name="CanceledError"}ee.inherits(ns,Ye,{__CANCEL__:!0});function Mp(o,e,r){const a=r.config.validateStatus;!r.status||!a||a(r.status)?o(r):e(new Ye("Request failed with status code "+r.status,[Ye.ERR_BAD_REQUEST,Ye.ERR_BAD_RESPONSE][Math.floor(r.status/100)-4],r.config,r.request,r))}function f0(o){const e=/^([-+\w]{1,25})(:?\/\/|:)/.exec(o);return e&&e[1]||""}function h0(o,e){o=o||10;const r=new Array(o),a=new Array(o);let i=0,l=0,c;return e=e!==void 0?e:1e3,function(f){const g=Date.now(),m=a[l];c||(c=g),r[i]=f,a[i]=g;let y=l,w=0;for(;y!==i;)w+=r[y++],y=y%o;if(i=(i+1)%o,i===l&&(l=(l+1)%o),g-c<e)return;const S=m&&g-m;return S?Math.round(w*1e3/S):void 0}}function g0(o,e){let r=0,a=1e3/e,i,l;const c=(g,m=Date.now())=>{r=m,i=null,l&&(clearTimeout(l),l=null),o(...g)};return[(...g)=>{const m=Date.now(),y=m-r;y>=a?c(g,m):(i=g,l||(l=setTimeout(()=>{l=null,c(i)},a-y)))},()=>i&&c(i)]}const ec=(o,e,r=3)=>{let a=0;const i=h0(50,250);return g0(l=>{const c=l.loaded,d=l.lengthComputable?l.total:void 0,f=c-a,g=i(f),m=c<=d;a=c;const y={loaded:c,total:d,progress:d?c/d:void 0,bytes:f,rate:g||void 0,estimated:g&&d&&m?(d-c)/g:void 0,event:l,lengthComputable:d!=null,[e?"download":"upload"]:!0};o(y)},r)},$p=(o,e)=>{const r=o!=null;return[a=>e[0]({lengthComputable:r,total:o,loaded:a}),e[1]]},Np=o=>(...e)=>ee.asap(()=>o(...e)),p0=Sn.hasStandardBrowserEnv?((o,e)=>r=>(r=new URL(r,Sn.origin),o.protocol===r.protocol&&o.host===r.host&&(e||o.port===r.port)))(new URL(Sn.origin),Sn.navigator&&/(msie|trident)/i.test(Sn.navigator.userAgent)):()=>!0,m0=Sn.hasStandardBrowserEnv?{write(o,e,r,a,i,l){const c=[o+"="+encodeURIComponent(e)];ee.isNumber(r)&&c.push("expires="+new Date(r).toGMTString()),ee.isString(a)&&c.push("path="+a),ee.isString(i)&&c.push("domain="+i),l===!0&&c.push("secure"),document.cookie=c.join("; ")},read(o){const e=document.cookie.match(new RegExp("(^|;\\s*)("+o+")=([^;]*)"));return e?decodeURIComponent(e[3]):null},remove(o){this.write(o,"",Date.now()-864e5)}}:{write(){},read(){return null},remove(){}};function y0(o){return/^([a-z][a-z\d+\-.]*:)?\/\//i.test(o)}function b0(o,e){return e?o.replace(/\/?\/$/,"")+"/"+e.replace(/^\/+/,""):o}function Ip(o,e,r){let a=!y0(e);return o&&(a||r==!1)?b0(o,e):e}const Hp=o=>o instanceof Rn?{...o}:o;function bi(o,e){e=e||{};const r={};function a(g,m,y,w){return ee.isPlainObject(g)&&ee.isPlainObject(m)?ee.merge.call({caseless:w},g,m):ee.isPlainObject(m)?ee.merge({},m):ee.isArray(m)?m.slice():m}function i(g,m,y,w){if(ee.isUndefined(m)){if(!ee.isUndefined(g))return a(void 0,g,y,w)}else return a(g,m,y,w)}function l(g,m){if(!ee.isUndefined(m))return a(void 0,m)}function c(g,m){if(ee.isUndefined(m)){if(!ee.isUndefined(g))return a(void 0,g)}else return a(void 0,m)}function d(g,m,y){if(y in e)return a(g,m);if(y in o)return a(void 0,g)}const f={url:l,method:l,data:l,baseURL:c,transformRequest:c,transformResponse:c,paramsSerializer:c,timeout:c,timeoutMessage:c,withCredentials:c,withXSRFToken:c,adapter:c,responseType:c,xsrfCookieName:c,xsrfHeaderName:c,onUploadProgress:c,onDownloadProgress:c,decompress:c,maxContentLength:c,maxBodyLength:c,beforeRedirect:c,transport:c,httpAgent:c,httpsAgent:c,cancelToken:c,socketPath:c,responseEncoding:c,validateStatus:d,headers:(g,m,y)=>i(Hp(g),Hp(m),y,!0)};return ee.forEach(Object.keys({...o,...e}),function(m){const y=f[m]||i,w=y(o[m],e[m],m);ee.isUndefined(w)&&y!==d||(r[m]=w)}),r}const jp=o=>{const e=bi({},o);let{data:r,withXSRFToken:a,xsrfHeaderName:i,xsrfCookieName:l,headers:c,auth:d}=e;if(e.headers=c=Rn.from(c),e.url=Tp(Ip(e.baseURL,e.url,e.allowAbsoluteUrls),o.params,o.paramsSerializer),d&&c.set("Authorization","Basic "+btoa((d.username||"")+":"+(d.password?unescape(encodeURIComponent(d.password)):""))),ee.isFormData(r)){if(Sn.hasStandardBrowserEnv||Sn.hasStandardBrowserWebWorkerEnv)c.setContentType(void 0);else if(ee.isFunction(r.getHeaders)){const f=r.getHeaders(),g=["content-type","content-length"];Object.entries(f).forEach(([m,y])=>{g.includes(m.toLowerCase())&&c.set(m,y)})}}if(Sn.hasStandardBrowserEnv&&(a&&ee.isFunction(a)&&(a=a(e
Line truncated
|
||||
`+c.map(Vp).join(`
|
||||
`):" "+Vp(c[0]):"as no adapter specified";throw new Ye("There is no suitable adapter to dispatch the request "+d,"ERR_NOT_SUPPORT")}return i},adapters:xd};function Td(o){if(o.cancelToken&&o.cancelToken.throwIfRequested(),o.signal&&o.signal.aborted)throw new ns(null,o)}function Kp(o){return Td(o),o.headers=Rn.from(o.headers),o.data=Ad.call(o,o.transformRequest),["post","put","patch"].indexOf(o.method)!==-1&&o.headers.setContentType("application/x-www-form-urlencoded",!1),Fp.getAdapter(o.adapter||fo.adapter,o)(o).then(function(a){return Td(o),a.data=Ad.call(o,o.transformResponse,a),a.headers=Rn.from(a.headers),a},function(a){return Lp(a)||(Td(o),a&&a.response&&(a.response.data=Ad.call(o,o.transformResponse,a.response),a.response.headers=Rn.from(a.response.headers))),Promise.reject(a)})}const Wp="1.12.2",nc={};["object","boolean","number","function","string","symbol"].forEach((o,e)=>{nc[o]=function(a){return typeof a===o||"a"+(e<1?"n ":" ")+o}});const Yp={};nc.transitional=function(e,r,a){function i(l,c){return"[Axios v"+Wp+"] Transitional option '"+l+"'"+c+(a?". "+a:"")}return(l,c,d)=>{if(e===!1)throw new Ye(i(c," has been removed"+(r?" in "+r:"")),Ye.ERR_DEPRECATED);return r&&!Yp[c]&&(Yp[c]=!0,console.warn(i(c," has been deprecated since v"+r+" and will be removed in the near future"))),e?e(l,c,d):!0}},nc.spelling=function(e){return(r,a)=>(console.warn(`${a} is likely a misspelling of ${e}`),!0)};function k0(o,e,r){if(typeof o!="object")throw new Ye("options must be an object",Ye.ERR_BAD_OPTION_VALUE);const a=Object.keys(o);let i=a.length;for(;i-- >0;){const l=a[i],c=e[l];if(c){const d=o[l],f=d===void 0||c(d,l,o);if(f!==!0)throw new Ye("option "+l+" must be "+f,Ye.ERR_BAD_OPTION_VALUE);continue}if(r!==!0)throw new Ye("Unknown option "+l,Ye.ERR_BAD_OPTION)}}const rc={assertOptions:k0,validators:nc},Hr=rc.validators;let vi=class{constructor(e){this.defaults=e||{},this.interceptors={request:new kp,response:new kp}}async request(e,r){try{return await this._request(e,r)}catch(a){if(a instanceof Error){let i={};Error.captureStackTrace?Error.captureStackTrace(i):i=new Error;const l=i.stack?i.stack.replace(/^.+\n/,""):"";try{a.stack?l&&!String(a.stack).endsWith(l.replace(/^.+\n.+\n/,""))&&(a.stack+=`
|
||||
`+l):a.stack=l}catch{}}throw a}}_request(e,r){typeof e=="string"?(r=r||{},r.url=e):r=e||{},r=bi(this.defaults,r);const{transitional:a,paramsSerializer:i,headers:l}=r;a!==void 0&&rc.assertOptions(a,{silentJSONParsing:Hr.transitional(Hr.boolean),forcedJSONParsing:Hr.transitional(Hr.boolean),clarifyTimeoutError:Hr.transitional(Hr.boolean)},!1),i!=null&&(ee.isFunction(i)?r.paramsSerializer={serialize:i}:rc.assertOptions(i,{encode:Hr.function,serialize:Hr.function},!0)),r.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?r.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:r.allowAbsoluteUrls=!0),rc.assertOptions(r,{baseUrl:Hr.spelling("baseURL"),withXsrfToken:Hr.spelling("withXSRFToken")},!0),r.method=(r.method||this.defaults.method||"get").toLowerCase();let c=l&&ee.merge(l.common,l[r.method]);l&&ee.forEach(["delete","get","head","post","put","patch","common"],v=>{delete l[v]}),r.headers=Rn.concat(c,l);const d=[];let f=!0;this.interceptors.request.forEach(function(A){typeof A.runWhen=="function"&&A.runWhen(r)===!1||(f=f&&A.synchronous,d.unshift(A.fulfilled,A.rejected))});const g=[];this.interceptors.response.forEach(function(A){g.push(A.fulfilled,A.rejected)});let m,y=0,w;if(!f){const v=[Kp.bind(this),void 0];for(v.unshift(...d),v.push(...g),w=v.length,m=Promise.resolve(r);y<w;)m=m.then(v[y++],v[y++]);return m}w=d.length;let S=r;for(;y<w;){const v=d[y++],A=d[y++];try{S=v(S)}catch(x){A.call(this,x);break}}try{m=Kp.call(this,S)}catch(v){return Promise.reject(v)}for(y=0,w=g.length;y<w;)m=m.then(g[y++],g[y++]);return m}getUri(e){e=bi(this.defaults,e);const r=Ip(e.baseURL,e.url,e.allowAbsoluteUrls);return Tp(r,e.params,e.paramsSerializer)}};ee.forEach(["delete","get","head","options"],function(e){vi.prototype[e]=function(r,a){return this.request(bi(a||{},{method:e,url:r,data:(a||{}).data}))}}),ee.forEach(["post","put","patch"],function(e){function r(a){return function(l,c,d){return this.request(bi(d||{},{method:e,headers:a?{"Content-Type":"multipart/form-data"}:{},url:l,data:c}))}}vi.prototype[e]=r(),vi.prototype[e+"Form"]=r(!0)});let R0=class $S{constructor(e){if(typeof e!="function")throw new TypeError("executor must be a function.");let r;this.promise=new Promise(function(l){r=l});const a=this;this.promise.then(i=>{if(!a._listeners)return;let l=a._listeners.length;for(;l-- >0;)a._listeners[l](i);a._listeners=null}),this.promise.then=i=>{let l;const c=new Promise(d=>{a.subscribe(d),l=d}).then(i);return c.cancel=function(){a.unsubscribe(l)},c},e(function(l,c,d){a.reason||(a.reason=new ns(l,c,d),r(a.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(e){if(this.reason){e(this.reason);return}this._listeners?this._listeners.push(e):this._listeners=[e]}unsubscribe(e){if(!this._listeners)return;const r=this._listeners.indexOf(e);r!==-1&&this._listeners.splice(r,1)}toAbortSignal(){const e=new AbortController,r=a=>{e.abort(a)};return this.subscribe(r),e.signal.unsubscribe=()=>this.unsubscribe(r),e.signal}static source(){let e;return{token:new $S(function(i){e=i}),cancel:e}}};function D0(o){return function(r){return o.apply(null,r)}}function O0(o){return ee.isObject(o)&&o.isAxiosError===!0}const kd={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(kd).forEach(([o,e])=>{kd[e]=o});function Xp(o){const e=new vi(o),r=fp(vi.prototype.request,e);return ee.extend(r,vi.prototype,e,{allOwnKeys:!0}),ee.extend(r,e,null,{allOwnKeys:!0}),r.create=function(i){return Xp(bi(o,i))},r}const Gt=Xp(fo);Gt.Axios=vi,Gt.CanceledError=ns,Gt.CancelToken=R0,Gt.isCancel=Lp,Gt.VERSION=Wp,Gt.toFormData=Ql,Gt.AxiosError=Ye,Gt.Cancel=Gt.CanceledError,Gt.all=function(e){return Promise.all(e)},Gt.spread=D0,Gt.isAxiosError=O0,Gt.mergeConfig=bi,Gt.AxiosHeaders=Rn,Gt.formToJSON=o=>Dp(ee.isHTMLForm(o)?new FormData(o):o),Gt.getAdapter=Fp.getAdapter,Gt.HttpStatusCode=kd,Gt.default=Gt
Line truncated
|
||||
* @license React
|
||||
* react-jsx-runtime.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var Qp;function I0(){if(Qp)return po;Qp=1;var o=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function r(a,i,l){var c=null;if(l!==void 0&&(c=""+l),i.key!==void 0&&(c=""+i.key),"key"in i){l={};for(var d in i)d!=="key"&&(l[d]=i[d])}else l=i;return i=l.ref,{$$typeof:o,type:a,key:c,ref:i!==void 0?i:null,props:l}}return po.Fragment=e,po.jsx=r,po.jsxs=r,po}var Zp;function H0(){return Zp||(Zp=1,Md.exports=I0()),Md.exports}var dt=H0();const j0=Jr({__proto__:null,default:Qr(dt)},[dt]),$d=ut("ParentContextProvider"),em=N.createContext(null);let sc=null;function Nd(){sc?($d.log("[triggerModalParentUpdate] 모달 부모 컨텍스트 업데이트 트리거"),sc()):$d.warn("[triggerModalParentUpdate] Provider가 아직 마운트되지 않음")}const tm=({children:o})=>{const[e,r]=N.useState(0),a=N.useRef(!0),i=N.useCallback(()=>{const c=window.__g7LayoutContextStack||[];return c[c.length-1]?.dataContext},[]);N.useEffect(()=>(a.current=!0,sc=()=>{a.current&&(r(c=>c+1),$d.log("[ParentContextProvider] 버전 업데이트됨"))},()=>{a.current=!1,sc=null}),[]);const l={version:e,getParentDataContext:i};return dt.jsx(em.Provider,{value:l,children:o})};function z0(){return N.useContext(em)}const j=ut("ActionDispatcher");function U0(o,e,r,a){return r||(Vg(o)||od(o)?"$t:core.errors.network_request_failed":a||`Failed to execute action: ${e}`)}const nm=new Set(["navigate","navigateBack","navigateForward","replaceUrl","refresh","logout"]),B0=new Set(["redirect"]);function Vn(){try{return window.G7Core?.devTools}catch{return}}class It extends Error{constructor(r,a,i){super(r);$(this,"unknownHandler",!1);this.action=a,this.originalError=i,this.name="ActionError"}}const rm={click:"onClick",change:"onChange",input:"onInput",submit:"onSubmit",focus:"onFocus",blur:"onBlur",keydown:"onKeyDown",keyup:"onKeyUp",keypress:"onKeyPress",mousedown:"onMouseDown",mouseup:"onMouseUp",mouseenter:"onMouseEnter",mouseleave:"onMouseLeave",scroll:"onScroll",dragstart:"onDragStart",drag:"onDrag",dragend:"onDragEnd",dragenter:"onDragEnter",dragover:"onDragOver",dragleave:"onDragLeave",drop:"onDrop"},fs=class fs{constructor(e={},r,a){$(this,"bindingEngine");$(this,"translationEngine");$(this,"translationContext");$(this,"customHandlers",new Map);$(this,"defaultContext");$(this,"globalStateUpdater");$(this,"errorHandlingSetup",!1);$(this,"debounceTimers",new Map);$(this,"pendingDebounceFlushers",new Map);$(this,"debounceAccumulatedValues",new Map);$(this,"globalHeaders",[]);$(this,"namedActions",{});$(this,"previewMode",!1);$(this,"intervals",new Map);this.bindingEngine=new _n,this.translationEngine=r,this.translationContext=a,this.defaultContext=e,this.registerDefaultHandlers(),this.setupErrorHandling()}setGlobalHeaders(e){this.globalHeaders=e||[]}setNamedActions(e){this.namedActions=e||{};const r=Vn();r?.isEnabled?.()&&r.setNamedActionDefinitions?.(this.namedActions),j.log("[setNamedActions] registered:",Object.keys(this.namedActions))}getNamedActions(){return this.namedActions}setPreviewMode(e){this.previewMode=e,j.log("Preview mode:",e?"enabled":"disabled")}isPreviewMode(){return this.previewMode}static getPreviewSuppressedHandlers(){return nm}static getPreviewSuppressedLayoutFeatures(){return B0}resolveActionRef(e){if(!e.actionRef)return e;const r=this.namedActions[e.actionRef];if(!r)return j.warn(`[resolveActionRef] named action not found: "${e.actionRef}"`),e;const a=Vn();a?.isEnabled?.()&&a.trackNamedActionRef?.({actionRefName:e.actionRef,resolvedHandler:r.handler,timestamp:Date.now()});const{actionRef:i,type:l,key:c,event:d,...f}=e,g={...r,...Object.fromEntries(Object.entries(f).filter(([,m])=>m!==void 0)),type:l??r.type};return c!==void 0&&(g.key=c),d!==void 0&&(g.event=d),g}matchesPattern(e,r){if(r==="*")return!0;const a=r.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(`^${a}$`).test(e)}getMatchingGlobalHeaders(e,r){const a={};for(const i of this.globalHeaders)this.matchesPattern(e,i.pattern)&&Object.entries(i.headers).forEach(([l,c])=>{const d=Zi(c,r,{skipCache:!0});d!=null&&d!==""&&(a[l]=String(d))});return a}setupErrorHandling(){if(this.errorHandlingSetup)return;Sr().setActionExecutor(async(r,a)=>{const i={type:"click",handler:r.handler,target:r.target,params:r.params,actions:r.actions},l={data:{...this.defaultContext.data,error:a.error}};return await this.dispatchAction(i,l)}),this.errorHandlingSetup=!0}registerDefaultHandlers(){this.registerHandler("refetchDataSource",async(r,a)=>{const i=r.params?.dataSourceId,l=r.params?.sync;if(!i){j.warn("refetchDataSource: dataSourceId is required");return}if(typeof window<"u"&&window.G7Core?.dataSource?.refetch){const c=window.G7Core?.state?.get?.()||{},d={...a.state?._global,...c},f=window.G7Core?.state?.getLocal?.()||{},g=a.data?._local||a.state||{},m={...f,...g},y=a.isolatedContext?.state;j.log("[refetchDataSource] localStateOverride:",m),await window.G7Core.dataSource.refetch(i,{...l?{sync:!0}:{},...d?{globalStateOverride:d}:{},...m?{localStLine truncated
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
@@ -47,20 +84,20 @@ Error generating stack: `+u.message+`
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
transition: background-color 0.2s;
|
||||
`,i.onmouseenter=()=>{i.style.backgroundColor="rgba(0, 0, 0, 0.2)"},i.onmouseleave=()=>{i.style.backgroundColor="rgba(0, 0, 0, 0.1)"};const l=()=>{n.parentNode&&n.parentNode.removeChild(n)};i.onclick=l;const c=d=>{this.isImeComposing(d)||d.key==="Escape"&&(l(),document.removeEventListener("keydown",c))};return document.addEventListener("keydown",c),n.onclick=d=>{d.target===n&&l()},a.appendChild(i),n.appendChild(a),document.body.appendChild(n),{overlay:n,layer:a,closeLayer:l}}getNestedProperty(e,n){return n.split(".").reduce((a,i)=>a&&a[i]!==void 0?a[i]:void 0,e)}async handleSaveToLocalStorage(e,n){const{key:a,value:i}=e;if(!a)return z.error("saveToLocalStorage: key parameter is required"),!1;try{const l=typeof i=="string"?i:JSON.stringify(i);return localStorage.setItem(a,l),z.log(`saveToLocalStorage: saved ${a}`,i),!0}catch(l){return z.error("saveToLocalStorage: failed to save",{key:a,error:l}),!1}}async handleLoadFromLocalStorage(e,n){const{key:a,target:i,stateKey:l,defaultValue:c}=e;if(!a)return z.error("loadFromLocalStorage: key parameter is required"),c;try{const d=localStorage.getItem(a);if(d===null)return z.log(`loadFromLocalStorage: no value found for ${a}, using default`),l&&n.setState&&n.setState({[l]:c}),c;let f;try{f=JSON.parse(d)}catch{f=d}return z.log(`loadFromLocalStorage: loaded ${a}`,f),l&&n.setState&&n.setState({[l]:f}),f}catch(d){return z.error("loadFromLocalStorage: failed to load",{key:a,error:d}),c}}async handleCustomAction(e,n){const a=this.customHandlers.get(e.handler);if(!a){if(this.previewMode){z.warn(`[Preview] Unknown action handler "${e.handler}" — skipped (preview mode)`);return}const i=new jt(`Unknown action handler: ${e.handler}`,e);throw i.unknownHandler=!0,i}return await a(e,n)}resolveParams(e,n){if(!e)return{};const a={};for(const[i,l]of Object.entries(e))i.includes("{{")&&z.warn(`[resolveParams] setState params의 키에 표현식이 포함되어 있습니다: "${i}". 키는 해석되지 않습니다. 배열 항목 수정은 .map()/.filter() 패턴을 사용하세요.`),typeof l=="string"&&l.includes("{{")?a[i]=this.evaluateExpression(l,n):typeof l=="string"?a[i]=l:Array.isArray(l)?a[i]=l.map(c=>typeof c=="string"&&c.includes("{{")?this.evaluateExpression(c,n):c):typeof l=="object"&&l!==null&&!Array.isArray(l)&&Object.getPrototypeOf(l)!==Object.prototype&&Object.getPrototypeOf(l)!==null?a[i]=l:typeof l=="object"&&l!==null?a[i]=this.resolveParams(l,n):a[i]=l;return a}resolveValue(e,n){return n?this.bindingEngine.resolveBindings(e,n,{skipCache:!0}):e}evaluateExpressions(e,n){let a={};for(const[i,l]of Object.entries(e))if(i==="..."){let c=l;typeof l=="string"&&l.includes("{{")&&(c=this.evaluateExpression(l,n)),c&&typeof c=="object"&&!Array.isArray(c)&&(a={...a,...c})}else typeof l=="string"&&l.includes("{{")?a[i]=this.evaluateExpression(l,n):typeof l=="object"&&l!==null&&!Array.isArray(l)?a[i]=this.evaluateExpressions(l,n):a[i]=l;return a}evaluateExpressionsIfNeeded(e,n){const a={};for(const[i,l]of Object.entries(e))typeof l=="string"&&l.includes("{{")?a[i]=this.evaluateExpression(l,n):a[i]=l;return a}evaluateExpression(e,n){if(!n)return e;const a=Aa(e);if(a===null){let c=n;if(e.includes("_global")){const f=window.G7Core?.state?.get()?._global;f&&(c={...n,_global:f})}if(e.includes("_computed")||e.includes("$computed")){const f=window.__g7ActionContext?.computedRef?.current;f&&Object.keys(f).length>0&&(c={...c,_computed:f,$computed:f})}return this.bindingEngine.resolveBindings(e,c,{skipCache:!0})}let i=a;i=i.replace(/\$args\.(\d+)/g,"$args[$1]");let l=n;if(i.includes("_global")){const d=window.G7Core?.state?.get()?._global;d&&(l={...n,_global:d})}if(i.includes("_computed")||i.includes("$computed")){const d=window.__g7ActionContext?.computedRef?.current;d&&Object.keys(d).length>0&&(l={...l,_computed:d,$computed:d})}i.includes("_global.modules")&&(z.log("[evaluateExpression] expression:",i),z.log("[evaluateExpression] dataContext._global:",l._global),z.log("[evaluateExpression] dataContext._global?.modules:",l._global?.modules));try{const c=Pn(i)?this.bindingEngine.evaluatePipeExpression(i,l,{skipCache:!0}):this.bindingEngine.evaluateExpression(i,l);return i.includes("_global.modules")&&z.log("[evaluateExpression] result:",c),typeof c=="string"&&c.startsWith("$t:")&&this.translationEngine&&this.translationContext?this.translationEngine.resolveTranslations(c,this.translationContext,n):c}catch(c){return z.error("Expression evaluation failed:",e,c),e}}setDefaultContext(e){this.defaultContext={...this.defaultContext,...e}}setGlobalStateUpdater(e){this.globalStateUpdater=e}getGlobalStateUpdater(){return this.globalStateUpdater}async dispatchAction(e,n){const a={...this.defaultContext,...n,data:{...this.defaultContext.data,...n?.data}};if(e.confirm){let i=this.resolveValue(e.confirm,a.data);if(this.translationEngine&&this.translationContext&&i.startsWith("$t:")&&(i=this.translationEngine.resolveTranslations(i,this.translationContext,a.data)),!confirm(i))return{success:!1}}try{r
Line truncated
|
||||
`,i.onmouseenter=()=>{i.style.backgroundColor="rgba(0, 0, 0, 0.2)"},i.onmouseleave=()=>{i.style.backgroundColor="rgba(0, 0, 0, 0.1)"};const l=()=>{r.parentNode&&r.parentNode.removeChild(r)};i.onclick=l;const c=d=>{this.isImeComposing(d)||d.key==="Escape"&&(l(),document.removeEventListener("keydown",c))};return document.addEventListener("keydown",c),r.onclick=d=>{d.target===r&&l()},a.appendChild(i),r.appendChild(a),document.body.appendChild(r),{overlay:r,layer:a,closeLayer:l}}getNestedProperty(e,r){return r.split(".").reduce((a,i)=>a&&a[i]!==void 0?a[i]:void 0,e)}async handleSaveToLocalStorage(e,r){const{key:a,value:i}=e;if(!a)return j.error("saveToLocalStorage: key parameter is required"),!1;try{const l=typeof i=="string"?i:JSON.stringify(i);return localStorage.setItem(a,l),j.log(`saveToLocalStorage: saved ${a}`,i),!0}catch(l){return j.error("saveToLocalStorage: failed to save",{key:a,error:l}),!1}}async handleLoadFromLocalStorage(e,r){const{key:a,target:i,stateKey:l,defaultValue:c}=e;if(!a)return j.error("loadFromLocalStorage: key parameter is required"),c;try{const d=localStorage.getItem(a);if(d===null)return j.log(`loadFromLocalStorage: no value found for ${a}, using default`),l&&r.setState&&r.setState({[l]:c}),c;let f;try{f=JSON.parse(d)}catch{f=d}return j.log(`loadFromLocalStorage: loaded ${a}`,f),l&&r.setState&&r.setState({[l]:f}),f}catch(d){return j.error("loadFromLocalStorage: failed to load",{key:a,error:d}),c}}async handleCustomAction(e,r){const a=this.customHandlers.get(e.handler);if(!a){if(this.previewMode){j.warn(`[Preview] Unknown action handler "${e.handler}" — skipped (preview mode)`);return}const i=new It(`Unknown action handler: ${e.handler}`,e);throw i.unknownHandler=!0,i}return await a(e,r)}resolveParams(e,r){if(!e)return{};const a={};for(const[i,l]of Object.entries(e))i.includes("{{")&&j.warn(`[resolveParams] setState params의 키에 표현식이 포함되어 있습니다: "${i}". 키는 해석되지 않습니다. 배열 항목 수정은 .map()/.filter() 패턴을 사용하세요.`),typeof l=="string"&&l.includes("{{")?a[i]=this.evaluateExpression(l,r):typeof l=="string"?a[i]=l:Array.isArray(l)?a[i]=l.map(c=>typeof c=="string"&&c.includes("{{")?this.evaluateExpression(c,r):c):typeof l=="object"&&l!==null&&!Array.isArray(l)&&Object.getPrototypeOf(l)!==Object.prototype&&Object.getPrototypeOf(l)!==null?a[i]=l:typeof l=="object"&&l!==null?a[i]=this.resolveParams(l,r):a[i]=l;return a}resolveValue(e,r){return r?this.bindingEngine.resolveBindings(e,r,{skipCache:!0}):e}evaluateExpressions(e,r){let a={};for(const[i,l]of Object.entries(e))if(i==="..."){let c=l;typeof l=="string"&&l.includes("{{")&&(c=this.evaluateExpression(l,r)),c&&typeof c=="object"&&!Array.isArray(c)&&(a={...a,...c})}else typeof l=="string"&&l.includes("{{")?a[i]=this.evaluateExpression(l,r):typeof l=="object"&&l!==null&&!Array.isArray(l)?a[i]=this.evaluateExpressions(l,r):a[i]=l;return a}evaluateExpressionsIfNeeded(e,r){const a={};for(const[i,l]of Object.entries(e))typeof l=="string"&&l.includes("{{")?a[i]=this.evaluateExpression(l,r):a[i]=l;return a}evaluateExpression(e,r){if(!r)return e;const a=Ta(e);if(a===null){let c=r;if(e.includes("_global")){const f=window.G7Core?.state?.get()?._global;f&&(c={...r,_global:f})}if(e.includes("_computed")||e.includes("$computed")){const f=window.__g7ActionContext?.computedRef?.current;f&&Object.keys(f).length>0&&(c={...c,_computed:f,$computed:f})}return this.bindingEngine.resolveBindings(e,c,{skipCache:!0})}let i=a;i=i.replace(/\$args\.(\d+)/g,"$args[$1]");let l=r;if(i.includes("_global")){const d=window.G7Core?.state?.get()?._global;d&&(l={...r,_global:d})}if(i.includes("_computed")||i.includes("$computed")){const d=window.__g7ActionContext?.computedRef?.current;d&&Object.keys(d).length>0&&(l={...l,_computed:d,$computed:d})}i.includes("_global.modules")&&(j.log("[evaluateExpression] expression:",i),j.log("[evaluateExpression] dataContext._global:",l._global),j.log("[evaluateExpression] dataContext._global?.modules:",l._global?.modules));try{const c=Gn(i)?this.bindingEngine.evaluatePipeExpression(i,l,{skipCache:!0}):this.bindingEngine.evaluateExpression(i,l);return i.includes("_global.modules")&&j.log("[evaluateExpression] result:",c),typeof c=="string"&&c.startsWith("$t:")&&this.translationEngine&&this.translationContext?this.translationEngine.resolveTranslations(c,this.translationContext,r):c}catch(c){return j.error("Expression evaluation failed:",e,c),e}}setDefaultContext(e){this.defaultContext={...this.defaultContext,...e}}setGlobalStateUpdater(e){this.globalStateUpdater=e}getGlobalStateUpdater(){return this.globalStateUpdater}async dispatchAction(e,r){const a={...this.defaultContext,...r,data:{...this.defaultContext.data,...r?.data}};if(e.confirm){let i=this.resolveValue(e.confirm,a.data);if(this.translationEngine&&this.translationContext&&i.startsWith("$t:")&&(i=this.translationEngine.resolveTranslations(i,this.translationContext,a.data)),!confirm(i))return{success:!1}}try{r
Line truncated
|
||||
To pick up a draggable item, press the space bar.
|
||||
While dragging, use the arrow keys to move the item.
|
||||
Press space again to drop the item in its new position, or press escape to cancel.
|
||||
`},GC={onDragStart(o){let{active:e}=o;return"Picked up draggable item "+e.id+"."},onDragOver(o){let{active:e,over:n}=o;return n?"Draggable item "+e.id+" was moved over droppable area "+n.id+".":"Draggable item "+e.id+" is no longer over a droppable area."},onDragEnd(o){let{active:e,over:n}=o;return n?"Draggable item "+e.id+" was dropped over droppable area "+n.id:"Draggable item "+e.id+" was dropped."},onDragCancel(o){let{active:e}=o;return"Dragging was cancelled. Draggable item "+e.id+" was dropped."}};function VC(o){let{announcements:e=GC,container:n,hiddenTextDescribedById:a,screenReaderInstructions:i=qC}=o;const{announce:l,announcement:c}=UC(),d=vo("DndLiveRegion"),[f,g]=N.useState(!1);if(N.useEffect(()=>{g(!0)},[]),BC(N.useMemo(()=>({onDragStart(b){let{active:S}=b;l(e.onDragStart({active:S}))},onDragMove(b){let{active:S,over:v}=b;e.onDragMove&&l(e.onDragMove({active:S,over:v}))},onDragOver(b){let{active:S,over:v}=b;l(e.onDragOver({active:S,over:v}))},onDragEnd(b){let{active:S,over:v}=b;l(e.onDragEnd({active:S,over:v}))},onDragCancel(b){let{active:S,over:v}=b;l(e.onDragCancel({active:S,over:v}))}}),[l,e])),!f)return null;const m=Ye.createElement(Ye.Fragment,null,Ye.createElement(zC,{id:a,value:i.draggable}),Ye.createElement(jC,{id:d,announcement:c}));return n?ka.createPortal(m,n):m}var tn;(function(o){o.DragStart="dragStart",o.DragMove="dragMove",o.DragEnd="dragEnd",o.DragCancel="dragCancel",o.DragOver="dragOver",o.RegisterDroppable="registerDroppable",o.SetDroppableDisabled="setDroppableDisabled",o.UnregisterDroppable="unregisterDroppable"})(tn||(tn={}));function ic(){}function Em(o,e){return N.useMemo(()=>({sensor:o,options:e??{}}),[o,e])}function FC(){for(var o=arguments.length,e=new Array(o),n=0;n<o;n++)e[n]=arguments[n];return N.useMemo(()=>[...e].filter(a=>a!=null),[...e])}const Sr=Object.freeze({x:0,y:0});function _m(o,e){return Math.sqrt(Math.pow(o.x-e.x,2)+Math.pow(o.y-e.y,2))}function Am(o,e){let{data:{value:n}}=o,{data:{value:a}}=e;return n-a}function KC(o,e){let{data:{value:n}}=o,{data:{value:a}}=e;return a-n}function xm(o){let{left:e,top:n,height:a,width:i}=o;return[{x:e,y:n},{x:e+i,y:n},{x:e,y:n+a},{x:e+i,y:n+a}]}function Tm(o,e){if(!o||o.length===0)return null;const[n]=o;return n[e]}function Rm(o,e,n){return e===void 0&&(e=o.left),n===void 0&&(n=o.top),{x:e+o.width*.5,y:n+o.height*.5}}const WC=o=>{let{collisionRect:e,droppableRects:n,droppableContainers:a}=o;const i=Rm(e,e.left,e.top),l=[];for(const c of a){const{id:d}=c,f=n.get(d);if(f){const g=_m(Rm(f),i);l.push({id:d,data:{droppableContainer:c,value:g}})}}return l.sort(Am)},YC=o=>{let{collisionRect:e,droppableRects:n,droppableContainers:a}=o;const i=xm(e),l=[];for(const c of a){const{id:d}=c,f=n.get(d);if(f){const g=xm(f),m=i.reduce((S,v,_)=>S+_m(g[_],v),0),b=Number((m/4).toFixed(4));l.push({id:d,data:{droppableContainer:c,value:b}})}}return l.sort(Am)};function XC(o,e){const n=Math.max(e.top,o.top),a=Math.max(e.left,o.left),i=Math.min(e.left+e.width,o.left+o.width),l=Math.min(e.top+e.height,o.top+o.height),c=i-a,d=l-n;if(a<i&&n<l){const f=e.width*e.height,g=o.width*o.height,m=c*d,b=m/(f+g-m);return Number(b.toFixed(4))}return 0}const JC=o=>{let{collisionRect:e,droppableRects:n,droppableContainers:a}=o;const i=[];for(const l of a){const{id:c}=l,d=n.get(c);if(d){const f=XC(d,e);f>0&&i.push({id:c,data:{droppableContainer:l,value:f}})}}return i.sort(KC)};function QC(o,e,n){return{...o,scaleX:e&&n?e.width/n.width:1,scaleY:e&&n?e.height/n.height:1}}function km(o,e){return o&&e?{x:o.left-e.left,y:o.top-e.top}:Sr}function ZC(o){return function(n){for(var a=arguments.length,i=new Array(a>1?a-1:0),l=1;l<a;l++)i[l-1]=arguments[l];return i.reduce((c,d)=>({...c,top:c.top+o*d.y,bottom:c.bottom+o*d.y,left:c.left+o*d.x,right:c.right+o*d.x}),{...n})}}const eE=ZC(1);function tE(o){if(o.startsWith("matrix3d(")){const e=o.slice(9,-1).split(/, /);return{x:+e[12],y:+e[13],scaleX:+e[0],scaleY:+e[5]}}else if(o.startsWith("matrix(")){const e=o.slice(7,-1).split(/, /);return{x:+e[4],y:+e[5],scaleX:+e[0],scaleY:+e[3]}}return null}function nE(o,e,n){const a=tE(e);if(!a)return o;const{scaleX:i,scaleY:l,x:c,y:d}=a,f=o.left-c-(1-i)*parseFloat(n),g=o.top-d-(1-l)*parseFloat(n.slice(n.indexOf(" ")+1)),m=i?o.width/i:o.width,b=l?o.height/l:o.height;return{width:m,height:b,top:g,right:f+m,bottom:g+b,left:f}}const rE={ignoreTransform:!1};function ds(o,e){e===void 0&&(e=rE);let n=o.getBoundingClientRect();if(e.ignoreTransform){const{transform:g,transformOrigin:m}=Ln(o).getComputedStyle(o);g&&(n=nE(n,g,m))}const{top:a,left:i,width:l,height:c,bottom:d,right:f}=n;return{top:a,left:i,width:l,height:c,bottom:d,right:f}}function Dm(o){return ds(o,{ignoreTransform:!0})}function aE(o){const e=o.innerWidth,n=o.innerHeight;return{top:0,left:0,right:e,bottom:n,width:e,height:n}}function iE(o,e){return e===void 0&&(e=Ln(o).getComputedStyle(o)),e.position==="fixed"}function sE(o,e){e===void 0&&(e=Ln(o).getComputedStyle(o));const n=/(auto|scroll|overlay)/;return["overflow","overflowX
Line truncated
|
||||
`},CC={onDragStart(o){let{active:e}=o;return"Picked up draggable item "+e.id+"."},onDragOver(o){let{active:e,over:r}=o;return r?"Draggable item "+e.id+" was moved over droppable area "+r.id+".":"Draggable item "+e.id+" is no longer over a droppable area."},onDragEnd(o){let{active:e,over:r}=o;return r?"Draggable item "+e.id+" was dropped over droppable area "+r.id:"Draggable item "+e.id+" was dropped."},onDragCancel(o){let{active:e}=o;return"Dragging was cancelled. Draggable item "+e.id+" was dropped."}};function EC(o){let{announcements:e=CC,container:r,hiddenTextDescribedById:a,screenReaderInstructions:i=wC}=o;const{announce:l,announcement:c}=bC(),d=So("DndLiveRegion"),[f,g]=N.useState(!1);if(N.useEffect(()=>{g(!0)},[]),vC(N.useMemo(()=>({onDragStart(y){let{active:w}=y;l(e.onDragStart({active:w}))},onDragMove(y){let{active:w,over:S}=y;e.onDragMove&&l(e.onDragMove({active:w,over:S}))},onDragOver(y){let{active:w,over:S}=y;l(e.onDragOver({active:w,over:S}))},onDragEnd(y){let{active:w,over:S}=y;l(e.onDragEnd({active:w,over:S}))},onDragCancel(y){let{active:w,over:S}=y;l(e.onDragCancel({active:w,over:S}))}}),[l,e])),!f)return null;const m=We.createElement(We.Fragment,null,We.createElement(mC,{id:a,value:i.draggable}),We.createElement(yC,{id:d,announcement:c}));return r?Oa.createPortal(m,r):m}var tn;(function(o){o.DragStart="dragStart",o.DragMove="dragMove",o.DragEnd="dragEnd",o.DragCancel="dragCancel",o.DragOver="dragOver",o.RegisterDroppable="registerDroppable",o.SetDroppableDisabled="setDroppableDisabled",o.UnregisterDroppable="unregisterDroppable"})(tn||(tn={}));function hc(){}function ym(o,e){return N.useMemo(()=>({sensor:o,options:e??{}}),[o,e])}function _C(){for(var o=arguments.length,e=new Array(o),r=0;r<o;r++)e[r]=arguments[r];return N.useMemo(()=>[...e].filter(a=>a!=null),[...e])}const wr=Object.freeze({x:0,y:0});function bm(o,e){return Math.sqrt(Math.pow(o.x-e.x,2)+Math.pow(o.y-e.y,2))}function vm(o,e){let{data:{value:r}}=o,{data:{value:a}}=e;return r-a}function AC(o,e){let{data:{value:r}}=o,{data:{value:a}}=e;return a-r}function Sm(o){let{left:e,top:r,height:a,width:i}=o;return[{x:e,y:r},{x:e+i,y:r},{x:e,y:r+a},{x:e+i,y:r+a}]}function wm(o,e){if(!o||o.length===0)return null;const[r]=o;return r[e]}function Cm(o,e,r){return e===void 0&&(e=o.left),r===void 0&&(r=o.top),{x:e+o.width*.5,y:r+o.height*.5}}const xC=o=>{let{collisionRect:e,droppableRects:r,droppableContainers:a}=o;const i=Cm(e,e.left,e.top),l=[];for(const c of a){const{id:d}=c,f=r.get(d);if(f){const g=bm(Cm(f),i);l.push({id:d,data:{droppableContainer:c,value:g}})}}return l.sort(vm)},TC=o=>{let{collisionRect:e,droppableRects:r,droppableContainers:a}=o;const i=Sm(e),l=[];for(const c of a){const{id:d}=c,f=r.get(d);if(f){const g=Sm(f),m=i.reduce((w,S,v)=>w+bm(g[v],S),0),y=Number((m/4).toFixed(4));l.push({id:d,data:{droppableContainer:c,value:y}})}}return l.sort(vm)};function kC(o,e){const r=Math.max(e.top,o.top),a=Math.max(e.left,o.left),i=Math.min(e.left+e.width,o.left+o.width),l=Math.min(e.top+e.height,o.top+o.height),c=i-a,d=l-r;if(a<i&&r<l){const f=e.width*e.height,g=o.width*o.height,m=c*d,y=m/(f+g-m);return Number(y.toFixed(4))}return 0}const RC=o=>{let{collisionRect:e,droppableRects:r,droppableContainers:a}=o;const i=[];for(const l of a){const{id:c}=l,d=r.get(c);if(d){const f=kC(d,e);f>0&&i.push({id:c,data:{droppableContainer:l,value:f}})}}return i.sort(AC)};function DC(o,e,r){return{...o,scaleX:e&&r?e.width/r.width:1,scaleY:e&&r?e.height/r.height:1}}function Em(o,e){return o&&e?{x:o.left-e.left,y:o.top-e.top}:wr}function OC(o){return function(r){for(var a=arguments.length,i=new Array(a>1?a-1:0),l=1;l<a;l++)i[l-1]=arguments[l];return i.reduce((c,d)=>({...c,top:c.top+o*d.y,bottom:c.bottom+o*d.y,left:c.left+o*d.x,right:c.right+o*d.x}),{...r})}}const LC=OC(1);function MC(o){if(o.startsWith("matrix3d(")){const e=o.slice(9,-1).split(/, /);return{x:+e[12],y:+e[13],scaleX:+e[0],scaleY:+e[5]}}else if(o.startsWith("matrix(")){const e=o.slice(7,-1).split(/, /);return{x:+e[4],y:+e[5],scaleX:+e[0],scaleY:+e[3]}}return null}function $C(o,e,r){const a=MC(e);if(!a)return o;const{scaleX:i,scaleY:l,x:c,y:d}=a,f=o.left-c-(1-i)*parseFloat(r),g=o.top-d-(1-l)*parseFloat(r.slice(r.indexOf(" ")+1)),m=i?o.width/i:o.width,y=l?o.height/l:o.height;return{width:m,height:y,top:g,right:f+m,bottom:g+y,left:f}}const NC={ignoreTransform:!1};function ls(o,e){e===void 0&&(e=NC);let r=o.getBoundingClientRect();if(e.ignoreTransform){const{transform:g,transformOrigin:m}=Dn(o).getComputedStyle(o);g&&(r=$C(r,g,m))}const{top:a,left:i,width:l,height:c,bottom:d,right:f}=r;return{top:a,left:i,width:l,height:c,bottom:d,right:f}}function _m(o){return ls(o,{ignoreTransform:!0})}function IC(o){const e=o.innerWidth,r=o.innerHeight;return{top:0,left:0,right:e,bottom:r,width:e,height:r}}function HC(o,e){return e===void 0&&(e=Dn(o).getComputedStyle(o)),e.position==="fixed"}function jC(o,e){e===void 0&&(e=Dn(o).getComputedStyle(o));const r=/(auto|scroll|overlay)/;return["overflow","overflowX
Line truncated
|
||||
<div style="padding: 1rem; border: 2px dashed #ccc; background: #f9f9f9; text-align: center;">
|
||||
<p style="margin: 0; color: #666;">Component not found: <strong>${e}</strong></p>
|
||||
</div>
|
||||
`,n}createErrorElement(e,n){const a=document.createElement("div");return a.className="component-error",a.setAttribute("data-component-type",e),a.innerHTML=`
|
||||
`,r}createErrorElement(e,r){const a=document.createElement("div");return a.className="component-error",a.setAttribute("data-component-type",e),a.innerHTML=`
|
||||
<div style="padding: 1rem; border: 2px solid #f44336; background: #ffebee; border-radius: 4px;">
|
||||
<p style="margin: 0 0 0.5rem 0; color: #c62828; font-weight: bold;">Component Error: ${e}</p>
|
||||
<p style="margin: 0; color: #666; font-size: 0.875rem;">${n?.message||"Unknown error"}</p>
|
||||
<p style="margin: 0; color: #666; font-size: 0.875rem;">${r?.message||"Unknown error"}</p>
|
||||
</div>
|
||||
`,a}renderErrorState(e,n){const a=n instanceof Error?n.message:String(n),i=n instanceof Cr?n.code:"UNKNOWN_ERROR";e.innerHTML=`
|
||||
`,a}renderErrorState(e,r){const a=r instanceof Error?r.message:String(r),i=r instanceof Er?r.code:"UNKNOWN_ERROR";e.innerHTML=`
|
||||
<div class="layout-error" data-error-code="${i}" style="
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
@@ -102,8 +139,14 @@ Error generating stack: `+u.message+`
|
||||
"
|
||||
>페이지 새로고침</button>
|
||||
</div>
|
||||
`,Gn.error("Error state rendered:",i,a)}getCurrentLayout(){return this.currentLayout}clear(){this.currentLayout=null,this.layoutCache.clear(),Gn.log("Layout data and cache cleared")}}const O_=ut("TranslationContext"),hf=N.createContext(null),mc=({children:o,translationEngine:e,translationContext:n})=>{const a=N.useMemo(()=>(l,c)=>{if(c){const d="|"+Object.entries(c).map(([f,g])=>`${f}=${g}`).join("|");return e.translate(l,n,d)}return e.translate(l,n)},[e,n]),i=N.useMemo(()=>({translationEngine:e,translationContext:n,t:a}),[e,n,a]);return dt.jsx(hf.Provider,{value:i,children:o})},ny=()=>{const o=N.useContext(hf);return o||(O_.warn("TranslationProvider 외부에서 호출되었습니다. 키를 그대로 반환합니다."),{t:e=>e,translationEngine:null,translationContext:null})},La=ut("SlotContext"),L_={registerToSlot:()=>{La.warn("SlotContext not available: registerToSlot called outside SlotProvider")},unregisterFromSlot:()=>{La.warn("SlotContext not available: unregisterFromSlot called outside SlotProvider")},getSlotComponents:()=>[],subscribeToSlot:()=>()=>{},clearAllSlots:()=>{},isEnabled:!1},M_=N.createContext(L_),gf=({children:o})=>{const e=N.useRef(new Map),n=N.useRef(new Map),a=N.useCallback(m=>{const b=n.current.get(m);b&&b.forEach(S=>{try{S()}catch(v){La.error(`Slot subscriber error (slotId: ${m}):`,v)}})},[]),i=N.useCallback((m,b,S)=>{if(!m||!b){La.warn("registerToSlot: slotId and componentId are required");return}e.current.has(m)||e.current.set(m,new Map);const v=e.current.get(m),_=v.get(b);_&&_.registrationKey===S.registrationKey||(v.set(b,S),La.log(`Component registered to slot: ${b} -> ${m} (order: ${S.order})`),a(m))},[a]),l=N.useCallback((m,b)=>{const S=e.current.get(m);S&&S.has(b)&&(S.delete(b),La.log(`Component unregistered from slot: ${b} <- ${m}`),S.size===0&&e.current.delete(m),a(m))},[a]),c=N.useCallback(m=>{const b=e.current.get(m);return b?Array.from(b.values()).sort((S,v)=>S.order-v.order):[]},[]),d=N.useCallback((m,b)=>{n.current.has(m)||n.current.set(m,new Set);const S=n.current.get(m);return S.add(b),()=>{S.delete(b),S.size===0&&n.current.delete(m)}},[]),f=N.useCallback(()=>{e.current.forEach((m,b)=>{a(b)}),e.current.clear(),n.current.clear(),La.log("All slots cleared")},[a]),g=N.useMemo(()=>({registerToSlot:i,unregisterFromSlot:l,getSlotComponents:c,subscribeToSlot:d,clearAllSlots:f,isEnabled:!0}),[i,l,c,d,f]);return N.useLayoutEffect(()=>{typeof window<"u"&&(window.__slotContextValue=g,La.log("SlotContext exposed to window.__slotContextValue"))},[g]),dt.jsx(M_.Provider,{value:g,children:o})};class pf{constructor(){this.notificationCreatedEvent=".Illuminate\\Notifications\\Events\\BroadcastNotificationCreated"}listenForWhisper(e,n){return this.listen(".client-"+e,n)}notification(e){return this.listen(this.notificationCreatedEvent,e)}stopListeningForNotification(e){return this.stopListening(this.notificationCreatedEvent,e)}stopListeningForWhisper(e,n){return this.stopListening(".client-"+e,n)}}class ry{constructor(e){this.namespace=e}format(e){return[".","\\"].includes(e.charAt(0))?e.substring(1):(this.namespace&&(e=this.namespace+"."+e),e.replace(/\./g,"\\"))}setNamespace(e){this.namespace=e}}function $_(o){try{return Reflect.construct(String,[],o),!0}catch{return!1}}class mf extends pf{constructor(e,n,a){super(),this.name=n,this.pusher=e,this.options=a,this.eventFormatter=new ry(this.options.namespace),this.subscribe()}subscribe(){this.subscription=this.pusher.subscribe(this.name)}unsubscribe(){this.pusher.unsubscribe(this.name)}listen(e,n){return this.on(this.eventFormatter.format(e),n),this}listenToAll(e){return this.subscription.bind_global((n,a)=>{if(n.startsWith("pusher:"))return;let i=String(this.options.namespace??"").replace(/\./g,"\\"),l=n.startsWith(i)?n.substring(i.length+1):"."+n;e(l,a)}),this}stopListening(e,n){return n?this.subscription.unbind(this.eventFormatter.format(e),n):this.subscription.unbind(this.eventFormatter.format(e)),this}stopListeningToAll(e){return e?this.subscription.unbind_global(e):this.subscription.unbind_global(),this}subscribed(e){return this.on("pusher:subscription_succeeded",()=>{e()}),this}error(e){return this.on("pusher:subscription_error",n=>{e(n)}),this}on(e,n){return this.subscription.bind(e,n),this}}class ay extends mf{whisper(e,n){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,n),this}}class N_ extends mf{whisper(e,n){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,n),this}}class I_ extends ay{here(e){return this.on("pusher:subscription_succeeded",n=>{e(Object.keys(n.members).map(a=>n.members[a]))}),this}joining(e){return this.on("pusher:member_added",n=>{e(n.info)}),this}whisper(e,n){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,n),this}leaving(e){return this.on("pusher:member_removed",n=>{e(n.info)}),this}}class iy extends pf{constructor(e,n,a){super(),this.events={},this.listeners={},this.name=n,this.socket=e,this.options=a,this.eventFormatter=new ry(this.op
Line truncated
|
||||
`);return O!==-1?(this.position+=O+1,C.slice(0,O)):null}isBufferTooLong(y){return this.position===y.length&&y.length>Ho}}var zo;(function(E){E[E.CONNECTING=0]="CONNECTING",E[E.OPEN=1]="OPEN",E[E.CLOSED=3]="CLOSED"})(zo||(zo={}));const sr=zo;var Tr=1;class jo{constructor(y,C){this.hooks=y,this.session=ys(1e3)+"/"+Ic(8),this.location=Uo(C),this.readyState=sr.CONNECTING,this.openStream()}send(y){return this.sendRaw(JSON.stringify([y]))}ping(){this.hooks.sendHeartbeat(this)}close(y,C){this.onClose(y,C,!0)}sendRaw(y){if(this.readyState===sr.OPEN)try{return je.createSocketRequest("POST",sa(Pa(this.location,this.session))).start(y),!0}catch{return!1}else return!1}reconnect(){this.closeStream(),this.openStream()}onClose(y,C,O){this.closeStream(),this.readyState=sr.CLOSED,this.onclose&&this.onclose({code:y,reason:C,wasClean:O})}onChunk(y){if(y.status===200){this.readyState===sr.OPEN&&this.onActivity();var C,O=y.data.slice(0,1);switch(O){case"o":C=JSON.parse(y.data.slice(1)||"{}"),this.onOpen(C);break;case"a":C=JSON.parse(y.data.slice(1)||"[]");for(var I=0;I<C.length;I++)this.onEvent(C[I]);break;case"m":C=JSON.parse(y.data.slice(1)||"null"),this.onEvent(C);break;case"h":this.hooks.onHeartbeat(this);break;case"c":C=JSON.parse(y.data.slice(1)||"[]"),this.onClose(C[0],C[1],!0);break}}}onOpen(y){this.readyState===sr.CONNECTING?(y&&y.hostname&&(this.location.base=Nc(this.location.base,y.hostname)),this.readyState=sr.OPEN,this.onopen&&this.onopen()):this.onClose(1006,"Server lost session",!0)}onEvent(y){this.readyState===sr.OPEN&&this.onmessage&&this.onmessage({data:y})}onActivity(){this.onactivity&&this.onactivity()}onError(y){this.onerror&&this.onerror(y)}openStream(){this.stream=je.createSocketRequest("POST",sa(this.hooks.getReceiveURL(this.location,this.session))),this.stream.bind("chunk",y=>{this.onChunk(y)}),this.stream.bind("finished",y=>{this.hooks.onFinished(this,y)}),this.stream.bind("buffer_too_long",()=>{this.reconnect()});try{this.stream.start()}catch(y){Te.defer(()=>{this.onError(y),this.onClose(1006,"Could not start streaming",!1)})}}closeStream(){this.stream&&(this.stream.unbind_all(),this.stream.close(),this.stream=null)}}function Uo(E){var y=/([^\?]*)\/*(\??.*)/.exec(E);return{base:y[1],queryString:y[2]}}function Pa(E,y){return E.base+"/"+y+"/xhr_send"}function sa(E){var y=E.indexOf("?")===-1?"?":"&";return E+y+"t="+ +new Date+"&n="+Tr++}function Nc(E,y){var C=/(https?:\/\/)([^\/:]+)((\/|:)?.*)/.exec(E);return C[1]+y+C[3]}function ys(E){return je.randomInt(E)}function Ic(E){for(var y=[],C=0;C<E;C++)y.push(ys(32).toString(32));return y.join("")}const Ai=jo;var Rr={getReceiveURL:function(E,y){return E.base+"/"+y+"/xhr_streaming"+E.queryString},onHeartbeat:function(E){E.sendRaw("[]")},sendHeartbeat:function(E){E.sendRaw("[]")},onFinished:function(E,y){E.onClose(1006,"Connection interrupted ("+y+")",!1)}};const Bo=Rr;var xi={getReceiveURL:function(E,y){return E.base+"/"+y+"/xhr"+E.queryString},onHeartbeat:function(){},sendHeartbeat:function(E){E.sendRaw("[]")},onFinished:function(E,y){y===200?E.reconnect():E.onClose(1006,"Connection interrupted ("+y+")",!1)}};const Vr=xi;var Po={getRequest:function(E){var y=je.getXHRAPI(),C=new y;return C.onreadystatechange=C.onprogress=function(){switch(C.readyState){case 3:C.responseText&&C.responseText.length>0&&E.onChunk(C.status,C.responseText);break;case 4:C.responseText&&C.responseText.length>0&&E.onChunk(C.status,C.responseText),E.emit("finished",C.status),E.close();break}},C},abortRequest:function(E){E.onreadystatechange=null,E.abort()}};const bs=Po;var Hc={createStreamingSocket(E){return this.createSocket(Bo,E)},createPollingSocket(E){return this.createSocket(Vr,E)},createSocket(E,y){return new Ai(E,y)},createXHR(E,y){return this.createRequest(bs,E,y)},createRequest(E,y,C){return new Of(E,y,C)}};const Ti=Hc;Ti.createXDR=function(E,y){return this.createRequest($c,E,y)};var zc={nextAuthCallbackID:1,auth_callbacks:{},ScriptReceivers:m,DependenciesReceivers:_,getDefaultStrategy:Ba,Transports:_n,transportConnectionInitializer:Df,HTTPFactory:Ti,TimelineTransport:sn,getXHRAPI(){return window.XMLHttpRequest},getWebSocketAPI(){return window.WebSocket||window.MozWebSocket},setup(E){window.Pusher=E;var y=()=>{this.onDocumentBody(E.ready)};window.JSON?y():A.load("json2",{},y)},getDocument(){return document},getProtocol(){return this.getDocument().location.protocol},getAuthorizers(){return{ajax:qe,jsonp:pn}},onDocumentBody(E){document.body?E():setTimeout(()=>{this.onDocumentBody(E)},0)},createJSONPRequest(E,y){return new ir(E,y)},createScriptRequest(E){return new ra(E)},getLocalStorage(){try{return window.localStorage}catch{return}},createXHR(){return this.getXHRAPI()?this.createXMLHttpRequest():this.createMicrosoftXHR()},createXMLHttpRequest(){var E=this.getXHRAPI();return new E},createMicrosoftXHR(){return new ActiveXObject("Microsoft.XMLHTTP")},getNetwork(){return $o},createWebSocket(E){var y=this.getWebSocketAPI();return new y(E)},createSocketRequest(E,y){if(this.isXHRSuppo
Line truncated
|
||||
`,Fn.error("Error state rendered:",i,a)}getCurrentLayout(){return this.currentLayout}clear(){this.currentLayout=null,this.layoutCache.clear(),Fn.log("Layout data and cache cleared")}}const c_=ut("TranslationContext"),lf=N.createContext(null),_c=({children:o,translationEngine:e,translationContext:r})=>{const a=N.useMemo(()=>(l,c)=>{if(c){const d="|"+Object.entries(c).map(([f,g])=>`${f}=${g}`).join("|");return e.translate(l,r,d)}return e.translate(l,r)},[e,r]),i=N.useMemo(()=>({translationEngine:e,translationContext:r,t:a}),[e,r,a]);return dt.jsx(lf.Provider,{value:i,children:o})},Xm=()=>{const o=N.useContext(lf);return o||(c_.warn("TranslationProvider 외부에서 호출되었습니다. 키를 그대로 반환합니다."),{t:e=>e,translationEngine:null,translationContext:null})},$a=ut("SlotContext"),u_={registerToSlot:()=>{$a.warn("SlotContext not available: registerToSlot called outside SlotProvider")},unregisterFromSlot:()=>{$a.warn("SlotContext not available: unregisterFromSlot called outside SlotProvider")},getSlotComponents:()=>[],subscribeToSlot:()=>()=>{},clearAllSlots:()=>{},isEnabled:!1},d_=N.createContext(u_),cf=({children:o})=>{const e=N.useRef(new Map),r=N.useRef(new Map),a=N.useCallback(m=>{const y=r.current.get(m);y&&y.forEach(w=>{try{w()}catch(S){$a.error(`Slot subscriber error (slotId: ${m}):`,S)}})},[]),i=N.useCallback((m,y,w)=>{if(!m||!y){$a.warn("registerToSlot: slotId and componentId are required");return}e.current.has(m)||e.current.set(m,new Map);const S=e.current.get(m),v=S.get(y);v&&v.registrationKey===w.registrationKey||(S.set(y,w),$a.log(`Component registered to slot: ${y} -> ${m} (order: ${w.order})`),a(m))},[a]),l=N.useCallback((m,y)=>{const w=e.current.get(m);w&&w.has(y)&&(w.delete(y),$a.log(`Component unregistered from slot: ${y} <- ${m}`),w.size===0&&e.current.delete(m),a(m))},[a]),c=N.useCallback(m=>{const y=e.current.get(m);return y?Array.from(y.values()).sort((w,S)=>w.order-S.order):[]},[]),d=N.useCallback((m,y)=>{r.current.has(m)||r.current.set(m,new Set);const w=r.current.get(m);return w.add(y),()=>{w.delete(y),w.size===0&&r.current.delete(m)}},[]),f=N.useCallback(()=>{e.current.forEach((m,y)=>{a(y)}),e.current.clear(),r.current.clear(),$a.log("All slots cleared")},[a]),g=N.useMemo(()=>({registerToSlot:i,unregisterFromSlot:l,getSlotComponents:c,subscribeToSlot:d,clearAllSlots:f,isEnabled:!0}),[i,l,c,d,f]);return N.useLayoutEffect(()=>{typeof window<"u"&&(window.__slotContextValue=g,$a.log("SlotContext exposed to window.__slotContextValue"))},[g]),dt.jsx(d_.Provider,{value:g,children:o})};class uf{constructor(){this.notificationCreatedEvent=".Illuminate\\Notifications\\Events\\BroadcastNotificationCreated"}listenForWhisper(e,r){return this.listen(".client-"+e,r)}notification(e){return this.listen(this.notificationCreatedEvent,e)}stopListeningForNotification(e){return this.stopListening(this.notificationCreatedEvent,e)}stopListeningForWhisper(e,r){return this.stopListening(".client-"+e,r)}}class Jm{constructor(e){this.namespace=e}format(e){return[".","\\"].includes(e.charAt(0))?e.substring(1):(this.namespace&&(e=this.namespace+"."+e),e.replace(/\./g,"\\"))}setNamespace(e){this.namespace=e}}function f_(o){try{new o}catch(e){if(e instanceof Error&&e.message.includes("is not a constructor"))return!1}return!0}class df extends uf{constructor(e,r,a){super(),this.name=r,this.pusher=e,this.options=a,this.eventFormatter=new Jm(this.options.namespace),this.subscribe()}subscribe(){this.subscription=this.pusher.subscribe(this.name)}unsubscribe(){this.pusher.unsubscribe(this.name)}listen(e,r){return this.on(this.eventFormatter.format(e),r),this}listenToAll(e){return this.subscription.bind_global((r,a)=>{if(r.startsWith("pusher:"))return;let i=String(this.options.namespace??"").replace(/\./g,"\\"),l=r.startsWith(i)?r.substring(i.length+1):"."+r;e(l,a)}),this}stopListening(e,r){return r?this.subscription.unbind(this.eventFormatter.format(e),r):this.subscription.unbind(this.eventFormatter.format(e)),this}stopListeningToAll(e){return e?this.subscription.unbind_global(e):this.subscription.unbind_global(),this}subscribed(e){return this.on("pusher:subscription_succeeded",()=>{e()}),this}error(e){return this.on("pusher:subscription_error",r=>{e(r)}),this}on(e,r){return this.subscription.bind(e,r),this}}class Qm extends df{whisper(e,r){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,r),this}}class h_ extends df{whisper(e,r){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,r),this}}class g_ extends Qm{here(e){return this.on("pusher:subscription_succeeded",r=>{e(Object.keys(r.members).map(a=>r.members[a]))}),this}joining(e){return this.on("pusher:member_added",r=>{e(r.info)}),this}whisper(e,r){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,r),this}leaving(e){return this.on("pusher:member_removed",r=>{e(r.info)}),this}}class Zm extends uf{constructor(e,r,a){super(),this.events={},this.listeners={},this.name=r,this.socket=e,this.op
Line truncated
|
||||
* Pusher JavaScript Library v8.4.0
|
||||
* https://pusher.com/
|
||||
*
|
||||
* Copyright 2020, Pusher
|
||||
* Released under the MIT licence.
|
||||
*/var ay;function S_(){return ay||(ay=1,(function(o,e){(function(a,i){o.exports=i()})(window,function(){return(function(r){var a={};function i(l){if(a[l])return a[l].exports;var c=a[l]={i:l,l:!1,exports:{}};return r[l].call(c.exports,c,c.exports,i),c.l=!0,c.exports}return i.m=r,i.c=a,i.d=function(l,c,d){i.o(l,c)||Object.defineProperty(l,c,{enumerable:!0,get:d})},i.r=function(l){typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(l,"__esModule",{value:!0})},i.t=function(l,c){if(c&1&&(l=i(l)),c&8||c&4&&typeof l=="object"&&l&&l.__esModule)return l;var d=Object.create(null);if(i.r(d),Object.defineProperty(d,"default",{enumerable:!0,value:l}),c&2&&typeof l!="string")for(var f in l)i.d(d,f,function(g){return l[g]}.bind(null,f));return d},i.n=function(l){var c=l&&l.__esModule?function(){return l.default}:function(){return l};return i.d(c,"a",c),c},i.o=function(l,c){return Object.prototype.hasOwnProperty.call(l,c)},i.p="",i(i.s=2)})([(function(r,a,i){var l=this&&this.__extends||(function(){var A=function(x,R){return A=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(M,O){M.__proto__=O}||function(M,O){for(var L in O)O.hasOwnProperty(L)&&(M[L]=O[L])},A(x,R)};return function(x,R){A(x,R);function M(){this.constructor=x}x.prototype=R===null?Object.create(R):(M.prototype=R.prototype,new M)}})();Object.defineProperty(a,"__esModule",{value:!0});var c=256,d=(function(){function A(x){x===void 0&&(x="="),this._paddingCharacter=x}return A.prototype.encodedLength=function(x){return this._paddingCharacter?(x+2)/3*4|0:(x*8+5)/6|0},A.prototype.encode=function(x){for(var R="",M=0;M<x.length-2;M+=3){var O=x[M]<<16|x[M+1]<<8|x[M+2];R+=this._encodeByte(O>>>18&63),R+=this._encodeByte(O>>>12&63),R+=this._encodeByte(O>>>6&63),R+=this._encodeByte(O>>>0&63)}var L=x.length-M;if(L>0){var O=x[M]<<16|(L===2?x[M+1]<<8:0);R+=this._encodeByte(O>>>18&63),R+=this._encodeByte(O>>>12&63),L===2?R+=this._encodeByte(O>>>6&63):R+=this._paddingCharacter||"",R+=this._paddingCharacter||""}return R},A.prototype.maxDecodedLength=function(x){return this._paddingCharacter?x/4*3|0:(x*6+7)/8|0},A.prototype.decodedLength=function(x){return this.maxDecodedLength(x.length-this._getPaddingLength(x))},A.prototype.decode=function(x){if(x.length===0)return new Uint8Array(0);for(var R=this._getPaddingLength(x),M=x.length-R,O=new Uint8Array(this.maxDecodedLength(M)),L=0,q=0,G=0,B=0,K=0,fe=0,Ce=0;q<M-4;q+=4)B=this._decodeChar(x.charCodeAt(q+0)),K=this._decodeChar(x.charCodeAt(q+1)),fe=this._decodeChar(x.charCodeAt(q+2)),Ce=this._decodeChar(x.charCodeAt(q+3)),O[L++]=B<<2|K>>>4,O[L++]=K<<4|fe>>>2,O[L++]=fe<<6|Ce,G|=B&c,G|=K&c,G|=fe&c,G|=Ce&c;if(q<M-1&&(B=this._decodeChar(x.charCodeAt(q)),K=this._decodeChar(x.charCodeAt(q+1)),O[L++]=B<<2|K>>>4,G|=B&c,G|=K&c),q<M-2&&(fe=this._decodeChar(x.charCodeAt(q+2)),O[L++]=K<<4|fe>>>2,G|=fe&c),q<M-3&&(Ce=this._decodeChar(x.charCodeAt(q+3)),O[L++]=fe<<6|Ce,G|=Ce&c),G!==0)throw new Error("Base64Coder: incorrect characters for decoding");return O},A.prototype._encodeByte=function(x){var R=x;return R+=65,R+=25-x>>>8&6,R+=51-x>>>8&-75,R+=61-x>>>8&-15,R+=62-x>>>8&3,String.fromCharCode(R)},A.prototype._decodeChar=function(x){var R=c;return R+=(42-x&x-44)>>>8&-c+x-43+62,R+=(46-x&x-48)>>>8&-c+x-47+63,R+=(47-x&x-58)>>>8&-c+x-48+52,R+=(64-x&x-91)>>>8&-c+x-65+0,R+=(96-x&x-123)>>>8&-c+x-97+26,R},A.prototype._getPaddingLength=function(x){var R=0;if(this._paddingCharacter){for(var M=x.length-1;M>=0&&x[M]===this._paddingCharacter;M--)R++;if(x.length<4||R>2)throw new Error("Base64Coder: incorrect padding")}return R},A})();a.Coder=d;var f=new d;function g(A){return f.encode(A)}a.encode=g;function m(A){return f.decode(A)}a.decode=m;var y=(function(A){l(x,A);function x(){return A!==null&&A.apply(this,arguments)||this}return x.prototype._encodeByte=function(R){var M=R;return M+=65,M+=25-R>>>8&6,M+=51-R>>>8&-75,M+=61-R>>>8&-13,M+=62-R>>>8&49,String.fromCharCode(M)},x.prototype._decodeChar=function(R){var M=c;return M+=(44-R&R-46)>>>8&-c+R-45+62,M+=(94-R&R-96)>>>8&-c+R-95+63,M+=(47-R&R-58)>>>8&-c+R-48+52,M+=(64-R&R-91)>>>8&-c+R-65+0,M+=(96-R&R-123)>>>8&-c+R-97+26,M},x})(d);a.URLSafeCoder=y;var w=new y;function S(A){return w.encode(A)}a.encodeURLSafe=S;function v(A){return w.decode(A)}a.decodeURLSafe=v,a.encodedLength=function(A){return f.encodedLength(A)},a.maxDecodedLength=function(A){return f.maxDecodedLength(A)},a.decodedLength=function(A){return f.decodedLength(A)}}),(function(r,a,i){Object.defineProperty(a,"__esModule",{value:!0});var l="utf8: invalid string",c="utf8: invalid source encoding";function d(m){for(var y=new Uint8Array(f(m)),w=0,S=0;S<m.length;S++){var v=m.charCodeAt(S);v<128?y[w++]=v:v<2048?(y[w++]=192|v>>6,y[w++]=128|v&63):v<55296?(y[w++]=224|v>>12,y[w++]=128|v>>6&63,y[w++]=128|v&63):(S++,v=(v&1023)<<10,v|=m.charCodeAt(S)&1023,v+=65536,y[w++]=240|v>>18,y[w++]=128|v>>12&63,y[w++]=128|v>>6&63,y[w++]=128|v&63)}return y}a.encode=d;function f(m){for(var y=0,w=0Line truncated
|
||||
`);return D!==-1?(this.position+=D+1,E.slice(0,D)):null}isBufferTooLong(b){return this.position===b.length&&b.length>Ua}}var ys;(function(_){_[_.CONNECTING=0]="CONNECTING",_[_.OPEN=1]="OPEN",_[_.CLOSED=3]="CLOSED"})(ys||(ys={}));var Vr=ys,Po=1;class _f{constructor(b,E){this.hooks=b,this.session=vs(1e3)+"/"+Ba(8),this.location=Af(E),this.readyState=Vr.CONNECTING,this.openStream()}send(b){return this.sendRaw(JSON.stringify([b]))}ping(){this.hooks.sendHeartbeat(this)}close(b,E){this.onClose(b,E,!0)}sendRaw(b){if(this.readyState===Vr.OPEN)try{return Ve.createSocketRequest("POST",or(bs(this.location,this.session))).start(b),!0}catch{return!1}else return!1}reconnect(){this.closeStream(),this.openStream()}onClose(b,E,D){this.closeStream(),this.readyState=Vr.CLOSED,this.onclose&&this.onclose({code:b,reason:E,wasClean:D})}onChunk(b){if(b.status===200){this.readyState===Vr.OPEN&&this.onActivity();var E,D=b.data.slice(0,1);switch(D){case"o":E=JSON.parse(b.data.slice(1)||"{}"),this.onOpen(E);break;case"a":E=JSON.parse(b.data.slice(1)||"[]");for(var I=0;I<E.length;I++)this.onEvent(E[I]);break;case"m":E=JSON.parse(b.data.slice(1)||"null"),this.onEvent(E);break;case"h":this.hooks.onHeartbeat(this);break;case"c":E=JSON.parse(b.data.slice(1)||"[]"),this.onClose(E[0],E[1],!0);break}}}onOpen(b){this.readyState===Vr.CONNECTING?(b&&b.hostname&&(this.location.base=qo(this.location.base,b.hostname)),this.readyState=Vr.OPEN,this.onopen&&this.onopen()):this.onClose(1006,"Server lost session",!0)}onEvent(b){this.readyState===Vr.OPEN&&this.onmessage&&this.onmessage({data:b})}onActivity(){this.onactivity&&this.onactivity()}onError(b){this.onerror&&this.onerror(b)}openStream(){this.stream=Ve.createSocketRequest("POST",or(this.hooks.getReceiveURL(this.location,this.session))),this.stream.bind("chunk",b=>{this.onChunk(b)}),this.stream.bind("finished",b=>{this.hooks.onFinished(this,b)}),this.stream.bind("buffer_too_long",()=>{this.reconnect()});try{this.stream.start()}catch(b){me.defer(()=>{this.onError(b),this.onClose(1006,"Could not start streaming",!1)})}}closeStream(){this.stream&&(this.stream.unbind_all(),this.stream.close(),this.stream=null)}}function Af(_){var b=/([^\?]*)\/*(\??.*)/.exec(_);return{base:b[1],queryString:b[2]}}function bs(_,b){return _.base+"/"+b+"/xhr_send"}function or(_){var b=_.indexOf("?")===-1?"?":"&";return _+b+"t="+ +new Date+"&n="+Po++}function qo(_,b){var E=/(https?:\/\/)([^\/:]+)((\/|:)?.*)/.exec(_);return E[1]+b+E[3]}function vs(_){return Ve.randomInt(_)}function Ba(_){for(var b=[],E=0;E<_;E++)b.push(vs(32).toString(32));return b.join("")}var Pa=_f,jc={getReceiveURL:function(_,b){return _.base+"/"+b+"/xhr_streaming"+_.queryString},onHeartbeat:function(_){_.sendRaw("[]")},sendHeartbeat:function(_){_.sendRaw("[]")},onFinished:function(_,b){_.onClose(1006,"Connection interrupted ("+b+")",!1)}},Go=jc,zc={getReceiveURL:function(_,b){return _.base+"/"+b+"/xhr"+_.queryString},onHeartbeat:function(){},sendHeartbeat:function(_){_.sendRaw("[]")},onFinished:function(_,b){b===200?_.reconnect():_.onClose(1006,"Connection interrupted ("+b+")",!1)}},xi=zc,Dr={getRequest:function(_){var b=Ve.getXHRAPI(),E=new b;return E.onreadystatechange=E.onprogress=function(){switch(E.readyState){case 3:E.responseText&&E.responseText.length>0&&_.onChunk(E.status,E.responseText);break;case 4:E.responseText&&E.responseText.length>0&&_.onChunk(E.status,E.responseText),_.emit("finished",E.status),_.close();break}},E},abortRequest:function(_){_.onreadystatechange=null,_.abort()}},Vo=Dr,Ti={createStreamingSocket(_){return this.createSocket(Go,_)},createPollingSocket(_){return this.createSocket(xi,_)},createSocket(_,b){return new Pa(_,b)},createXHR(_,b){return this.createRequest(Vo,_,b)},createRequest(_,b,E){return new Ef(_,b,E)}},Or=Ti;Or.createXDR=function(_,b){return this.createRequest(Hc,_,b)};var Fo=Or,Ss={nextAuthCallbackID:1,auth_callbacks:{},ScriptReceivers:c,DependenciesReceivers:m,getDefaultStrategy:Bo,Transports:Ot,transportConnectionInitializer:za,HTTPFactory:Fo,TimelineTransport:aa,getXHRAPI(){return window.XMLHttpRequest},getWebSocketAPI(){return window.WebSocket||window.MozWebSocket},setup(_){window.Pusher=_;var b=()=>{this.onDocumentBody(_.ready)};window.JSON?b():y.load("json2",{},b)},getDocument(){return document},getProtocol(){return this.getDocument().location.protocol},getAuthorizers(){return{ajax:Ce,jsonp:Ln}},onDocumentBody(_){document.body?_():setTimeout(()=>{this.onDocumentBody(_)},0)},createJSONPRequest(_,b){return new xn(_,b)},createScriptRequest(_){return new An(_)},getLocalStorage(){try{return window.localStorage}catch{return}},createXHR(){return this.getXHRAPI()?this.createXMLHttpRequest():this.createMicrosoftXHR()},createXMLHttpRequest(){var _=this.getXHRAPI();return new _},createMicrosoftXHR(){return new ActiveXObject("Microsoft.XMLHTTP")},getNetwork(){return $t},createWebSocket(_){var b=this.getWebSocketAPI();return new b(_)},createSocketRequest(_,b){if(this.isXHRSupported())return this.HTTPFactory.createXHR(_
Line truncated
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -178,23 +221,23 @@ Error generating stack: `+u.message+`
|
||||
<div style="${c.card}">
|
||||
<!-- 아이콘 -->
|
||||
<div style="${c.iconContainer}">
|
||||
<i class="fa-solid ${n.icon}" style="${c.icon}"></i>
|
||||
<i class="fa-solid ${r.icon}" style="${c.icon}"></i>
|
||||
</div>
|
||||
|
||||
<!-- 제목 -->
|
||||
<h1 style="${c.title}">
|
||||
${this.escapeHtml(n.title)}
|
||||
${this.escapeHtml(r.title)}
|
||||
</h1>
|
||||
|
||||
<!-- 사용자 메시지 -->
|
||||
<p style="${c.message}">
|
||||
${this.escapeHtml(n.message)}
|
||||
${this.escapeHtml(r.message)}
|
||||
</p>
|
||||
|
||||
<!-- 상세 메시지 (API 응답 등) -->
|
||||
${n.detailMessage?`
|
||||
${r.detailMessage?`
|
||||
<p style="${c.detailMessage}">
|
||||
${this.escapeHtml(n.detailMessage)}
|
||||
${this.escapeHtml(r.detailMessage)}
|
||||
</p>
|
||||
`:`<div style="${c.spacer}"></div>`}
|
||||
|
||||
@@ -204,12 +247,12 @@ Error generating stack: `+u.message+`
|
||||
<p style="${c.stackTitle}">
|
||||
Stack Trace:
|
||||
</p>
|
||||
<pre style="${c.stackTrace}">${this.escapeHtml(n.stackTrace)}</pre>
|
||||
<pre style="${c.stackTrace}">${this.escapeHtml(r.stackTrace)}</pre>
|
||||
</div>
|
||||
`:""}
|
||||
|
||||
<!-- 새로고침 버튼 (복구 가능할 때만) -->
|
||||
${n.showReloadButton?`
|
||||
${r.showReloadButton?`
|
||||
<div style="${c.buttonContainer}">
|
||||
<button
|
||||
onclick="window.location.reload()"
|
||||
@@ -224,7 +267,7 @@ Error generating stack: `+u.message+`
|
||||
`:""}
|
||||
</div>
|
||||
</div>
|
||||
`}static renderFromError(e,n,a,i=!1,l,c,d){let f,g;if(n.userMessageKey.startsWith("$t:"))if(c&&l)try{f=c.translate(n.userMessageKey,l)}catch{f=n.userMessageKey.replace("$t:","")}else f=n.userMessageKey.replace("$t:","");else f=n.userMessageKey;if(d){const m=d;m.details?.apiMessage?g=m.details.apiMessage:m.response?.data?.message&&(g=m.response.data.message)}this.render(e,{title:a,message:f,detailMessage:g,icon:n.icon,showStack:n.showStack,stackTrace:n.stack,showReloadButton:n.recoverable,debug:i})}static escapeHtml(e){if(!e)return"";const n=document.createElement("div");return n.textContent=e,n.innerHTML}}const fs=ut("Router");class X_{constructor(e){$(this,"routes",[]);$(this,"templateIdentifier");$(this,"eventHandlers",new Map);$(this,"pendingNavigation",null);$(this,"isNavigating",!1);$(this,"handlePopState",()=>{this.navigateToCurrentPath()});this.templateIdentifier=e,this.initPopstateListener()}initPopstateListener(){window.addEventListener("popstate",this.handlePopState)}on(e,n){this.eventHandlers.has(e)||this.eventHandlers.set(e,[]),this.eventHandlers.get(e).push(n)}async emit(e,...n){const a=this.eventHandlers.get(e);a&&await Promise.all(a.map(i=>i(...n)))}async loadRoutes(e){try{const n=e!==void 0&&e>0?e:null,a=await fetch($r(`/api/templates/${this.templateIdentifier}/routes`,"json",n));if(!a.ok)throw new Error(`Failed to load routes: ${a.statusText}`);const i=await a.json();if(!i.success)throw new Error("Failed to load routes from API");if(i.data&&Array.isArray(i.data.routes))this.routes=i.data.routes,fs.log(`Loaded ${this.routes.length} routes${e?` (v=${e})`:""}`);else throw new Error("Invalid routes data format")}catch(n){throw fs.error("Error loading routes:",n),n}}setRoutes(e){this.routes=e,fs.log(`Set ${this.routes.length} routes`)}match(e){const n=this.normalizePathname(e);for(const a of this.routes){const i=this.matchPattern(a.path,n);if(i!==null)return{route:a,params:i}}return null}normalizePathname(e){return e.length>1&&e.endsWith("/")?e.replace(/\/+$/,"")||"/":e}matchPattern(e,n){const a=[];let i=e.replace(/:([^/]+)/g,(f,g)=>(a.push(g),"([^/]+)"));i.startsWith("*/")&&(i="(?:/[^/]+)?"+i.slice(1)),i=i.replace(/\//g,"\\/");const l=new RegExp(`^${i}$`),c=n.match(l);if(!c)return null;const d={};return a.forEach((f,g)=>{d[f]=c[g+1]}),d}getRoutes(){return[...this.routes]}getAuthType(e,n){return e.auth_type?e.auth_type:n.startsWith("/admin")?"admin":"user"}async navigateToCurrentPath(){const e=window.location.pathname,n=window.location.search,a=this.match(e);if(!a){fs.warn(`No route matched for path: ${e}`),this.emit("routeNotFound",e);return}if(a.route.redirect){fs.log(`Redirecting from ${e} to ${a.route.redirect}`),this.navigate(a.route.redirect);return}if(a.route.auth_required){const c=this.getAuthType(a.route,e),d=yr.getInstance();let f=d.isAuthenticated()&&d.getAuthType()===c;if(f||(f=await d.checkAuth(c)),!f){const g=d.getLoginRedirectUrl(c,e+n);fs.log(`Not authenticated, redirecting to: ${g}`),window.location.href=g;return}}const i=new URLSearchParams(n),l={};for(const c of i.keys()){if(c in l)continue;const d=i.getAll(c);d.length>1?l[c]=d:d.length===1&&(c.endsWith("[]")?l[c]=d:l[c]=d[0])}await this.emit("routeChange",{path:e,layout:a.route.layout,endpoint:a.route.endpoint,params:{...a.route.params||{},...a.params},query:l,auth_required:a.route.auth_required,auth_type:a.route.auth_type,meta:a.route.meta})}navigate(e){if(this.isNavigating){this.pendingNavigation=e;return}this.executeNavigation(e)}async executeNavigation(e){this.isNavigating=!0,this.pendingNavigation=null;try{window.history.pushState({},"",e),await this.navigateToCurrentPath()}finally{if(this.isNavigating=!1,this.pendingNavigation){const n=this.pendingNavigation;this.executeNavigation(n)}}}}const J_=/^\/admin\/layout-editor\/([^/?#]+)\/?$/;function my(o){const e=J_.exec(o);return e?{templateIdentifier:e[1]}:null}const Bt=ut("ErrorPageHandler");class Q_{constructor(e){$(this,"templateId");$(this,"layoutLoader");$(this,"locale");$(this,"debug");$(this,"renderFunction");$(this,"dataSourceManager");$(this,"globalState");$(this,"errorConfig",null);$(this,"configLoaded",!1);this.templateId=e.templateId,this.layoutLoader=e.layoutLoader,this.locale=e.locale,this.debug=e.debug,this.renderFunction=e.renderFunction,this.dataSourceManager=e.dataSourceManager,this.globalState=e.globalState||{}}async loadConfig(){if(this.configLoaded)return this.errorConfig;try{const e=await fetch($r(`/api/templates/${this.templateId}/config`,"json"));if(!e.ok)return this.debug&&Bt.warn("Failed to load template config:",e.statusText),this.configLoaded=!0,null;const n=await e.json();if(!n.success||!n.data)return this.debug&&Bt.warn("Invalid template config response"),this.configLoaded=!0,null;const a=n.data;return!a.error_config||!a.error_config.layouts?(this.debug&&Bt.warn("No error_config found in template.json"),this.configLoaded=!0,null):(this.errorConfig=a.error_config,this.configLoaded=!0,this.debug&&Bt.log("Error config loaded:",this.error
Line truncated
|
||||
`}static renderFromError(e,r,a,i=!1,l,c,d){let f,g;if(r.userMessageKey.startsWith("$t:"))if(c&&l)try{f=c.translate(r.userMessageKey,l)}catch{f=r.userMessageKey.replace("$t:","")}else f=r.userMessageKey.replace("$t:","");else f=r.userMessageKey;if(d){const m=d;m.details?.apiMessage?g=m.details.apiMessage:m.response?.data?.message&&(g=m.response.data.message)}this.render(e,{title:a,message:f,detailMessage:g,icon:r.icon,showStack:r.showStack,stackTrace:r.stack,showReloadButton:r.recoverable,debug:i})}static escapeHtml(e){if(!e)return"";const r=document.createElement("div");return r.textContent=e,r.innerHTML}}const cs=ut("Router");class k_{constructor(e){$(this,"routes",[]);$(this,"templateIdentifier");$(this,"eventHandlers",new Map);$(this,"pendingNavigation",null);$(this,"isNavigating",!1);$(this,"handlePopState",()=>{this.navigateToCurrentPath()});this.templateIdentifier=e,this.initPopstateListener()}initPopstateListener(){window.addEventListener("popstate",this.handlePopState)}on(e,r){this.eventHandlers.has(e)||this.eventHandlers.set(e,[]),this.eventHandlers.get(e).push(r)}async emit(e,...r){const a=this.eventHandlers.get(e);a&&await Promise.all(a.map(i=>i(...r)))}async loadRoutes(e){try{const r=e!==void 0&&e>0?e:null,a=await fetch(Nr(`/api/templates/${this.templateIdentifier}/routes`,"json",r));if(!a.ok)throw new Error(`Failed to load routes: ${a.statusText}`);const i=await a.json();if(!i.success)throw new Error("Failed to load routes from API");if(i.data&&Array.isArray(i.data.routes))this.routes=i.data.routes,cs.log(`Loaded ${this.routes.length} routes${e?` (v=${e})`:""}`);else throw new Error("Invalid routes data format")}catch(r){throw cs.error("Error loading routes:",r),r}}setRoutes(e){this.routes=e,cs.log(`Set ${this.routes.length} routes`)}match(e){const r=this.normalizePathname(e);for(const a of this.routes){const i=this.matchPattern(a.path,r);if(i!==null)return{route:a,params:i}}return null}normalizePathname(e){return e.length>1&&e.endsWith("/")?e.replace(/\/+$/,"")||"/":e}matchPattern(e,r){const a=[];let i=e.replace(/:([^/]+)/g,(f,g)=>(a.push(g),"([^/]+)"));i.startsWith("*/")&&(i="(?:/[^/]+)?"+i.slice(1)),i=i.replace(/\//g,"\\/");const l=new RegExp(`^${i}$`),c=r.match(l);if(!c)return null;const d={};return a.forEach((f,g)=>{d[f]=c[g+1]}),d}getRoutes(){return[...this.routes]}getAuthType(e,r){return e.auth_type?e.auth_type:r.startsWith("/admin")?"admin":"user"}async navigateToCurrentPath(){const e=window.location.pathname,r=window.location.search,a=this.match(e);if(!a){cs.warn(`No route matched for path: ${e}`),this.emit("routeNotFound",e);return}if(a.route.redirect){cs.log(`Redirecting from ${e} to ${a.route.redirect}`),this.navigate(a.route.redirect);return}if(a.route.auth_required){const c=this.getAuthType(a.route,e),d=br.getInstance();let f=d.isAuthenticated()&&d.getAuthType()===c;if(f||(f=await d.checkAuth(c)),!f){const g=d.getLoginRedirectUrl(c,e+r);cs.log(`Not authenticated, redirecting to: ${g}`),window.location.href=g;return}}const i=new URLSearchParams(r),l={};for(const c of i.keys()){if(c in l)continue;const d=i.getAll(c);d.length>1?l[c]=d:d.length===1&&(c.endsWith("[]")?l[c]=d:l[c]=d[0])}await this.emit("routeChange",{path:e,layout:a.route.layout,endpoint:a.route.endpoint,params:{...a.route.params||{},...a.params},query:l,auth_required:a.route.auth_required,auth_type:a.route.auth_type,meta:a.route.meta})}navigate(e){if(this.isNavigating){this.pendingNavigation=e;return}this.executeNavigation(e)}async executeNavigation(e){this.isNavigating=!0,this.pendingNavigation=null;try{window.history.pushState({},"",e),await this.navigateToCurrentPath()}finally{if(this.isNavigating=!1,this.pendingNavigation){const r=this.pendingNavigation;this.executeNavigation(r)}}}}const R_=/^\/admin\/layout-editor\/([^/?#]+)\/?$/;function uy(o){const e=R_.exec(o);return e?{templateIdentifier:e[1]}:null}const jt=ut("ErrorPageHandler");class D_{constructor(e){$(this,"templateId");$(this,"layoutLoader");$(this,"locale");$(this,"debug");$(this,"renderFunction");$(this,"dataSourceManager");$(this,"globalState");$(this,"errorConfig",null);$(this,"configLoaded",!1);this.templateId=e.templateId,this.layoutLoader=e.layoutLoader,this.locale=e.locale,this.debug=e.debug,this.renderFunction=e.renderFunction,this.dataSourceManager=e.dataSourceManager,this.globalState=e.globalState||{}}async loadConfig(){if(this.configLoaded)return this.errorConfig;try{const e=await fetch(Nr(`/api/templates/${this.templateId}/config`,"json"));if(!e.ok)return this.debug&&jt.warn("Failed to load template config:",e.statusText),this.configLoaded=!0,null;const r=await e.json();if(!r.success||!r.data)return this.debug&&jt.warn("Invalid template config response"),this.configLoaded=!0,null;const a=r.data;return!a.error_config||!a.error_config.layouts?(this.debug&&jt.warn("No error_config found in template.json"),this.configLoaded=!0,null):(this.errorConfig=a.error_config,this.configLoaded=!0,this.debug&&jt.log("Error config loaded:",this.error
Line truncated
|
||||
with(ctx) {
|
||||
try {
|
||||
return Boolean(${a});
|
||||
@@ -232,7 +275,7 @@ Error generating stack: `+u.message+`
|
||||
return false;
|
||||
}
|
||||
}
|
||||
`)(n)}return!!e}catch(a){return U.warn(`Failed to evaluate script condition: ${e}`,a),!1}}showInitError(e){if(ld()){U.warn("Init failed while document is unloading — skipping error screen",e);return}const n=py(e);vf.renderFromError("app",n,"초기화 실패",this.config.debug,{templateId:this.config.templateId,locale:this.config.locale},void 0,e)}showTransitionOverlay(e,n){const a=typeof e=="boolean"?{enabled:e,style:"opaque",target:void 0,skeleton:void 0,spinner:void 0}:{enabled:e.enabled,style:e.style||"opaque",target:e.target,skeleton:e.skeleton,spinner:e.spinner,fallback_target:e.fallback_target};if(!a.enabled)return;if(this.hideTransitionOverlay(),a.style==="skeleton"&&a.skeleton?.component&&a.target&&n){this.renderSkeletonOverlay(a.target,a.skeleton,n,a.fallback_target);return}if(a.style==="spinner"&&a.target){this.renderSpinnerOverlay(a.target,a.spinner,a.fallback_target);return}const i=document.documentElement.classList.contains("dark");let l,c="";switch(a.style){case"blur":l=i?"rgba(17,24,39,0.3)":"rgba(255,255,255,0.3)",c="backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);";break;case"fade":l=i?"rgba(17,24,39,0.8)":"rgba(255,255,255,0.8)";break;case"skeleton":l=i?"rgb(17,24,39)":"rgb(249,250,251)";break;default:l=i?"rgb(17,24,39)":"rgb(249,250,251)";break}if(a.target){const d=`#${CSS.escape(a.target)}`,f=document.createElement("style");f.id="g7-transition-overlay",f.textContent=`${d}{position:relative;z-index:0;}${d}::after{content:'';position:absolute;inset:0;background:${l};${c}z-index:2147483647;pointer-events:none;}`,document.head.appendChild(f),this.transitionOverlayEl=f}else{const d=document.createElement("div");d.id="g7-transition-overlay",d.setAttribute("aria-hidden","true"),d.style.position="fixed",d.style.inset="0",d.style.zIndex="9999",d.style.pointerEvents="none",d.style.background=l;for(const f of c.split(";")){const g=f.indexOf(":");g!==-1&&d.style.setProperty(f.slice(0,g).trim(),f.slice(g+1).trim())}document.body.appendChild(d),this.transitionOverlayEl=d}}hideTransitionOverlay(){this.transitionOverlayEl&&(this.transitionOverlayEl.remove(),this.transitionOverlayEl=null),this.hideSkeletonOverlay()}renderSkeletonOverlay(e,n,a,i){const c=pr.getInstance().getComponent(n.component);if(!c){U.log(`Skeleton component "${n.component}" not found in registry, falling back to opaque overlay`),this.showTransitionOverlay({enabled:!0,style:"opaque",target:e});return}let d=document.getElementById(e),f="target";if(!d&&i&&(d=document.getElementById(i),f="fallback"),d||(d=document.getElementById("app"),f="fullpage"),!d){U.log(`Skeleton overlay: no target found (target="#${e}", fallback="${i||"none"}"), falling back to opaque overlay`),this.showTransitionOverlay({enabled:!0,style:"opaque",target:e});return}this.hideSkeletonOverlay();const m=document.documentElement.classList.contains("dark")?"rgb(17,24,39)":"rgb(249,250,251)",b=f==="fullpage"?"app":f==="fallback"?i:e,S=`#${CSS.escape(b)}`,v=document.createElement("style");v.id="g7-skeleton-overlay-style",v.textContent=`${S}{position:relative;z-index:0;}${S}::after{content:'';position:absolute;inset:0;background:${m};z-index:2147483646;pointer-events:none;}`,document.head.appendChild(v),this.transitionOverlayEl=v;const _=document.createElement("div");if(_.id="g7-skeleton-overlay",_.setAttribute("role","status"),_.setAttribute("aria-busy","true"),_.setAttribute("aria-label","Loading..."),f==="fullpage")_.style.cssText=["position:fixed","inset:0","z-index:20","overflow:hidden","pointer-events:none",`background:${m}`].join(";")+";";else{const R=d.getBoundingClientRect(),D=window.scrollX||document.documentElement.scrollLeft,B=window.scrollY||document.documentElement.scrollTop;_.style.cssText=["position:absolute",`top:${R.top+B}px`,`left:${R.left+D}px`,`width:${R.width}px`,`height:${R.height}px`,"z-index:20","overflow:hidden","pointer-events:none",`background:${m}`].join(";")+";"}document.body.appendChild(_),this.skeletonOverlayContainer=_;const A=a.components||[];let x;if(f==="fullpage")x=A;else{const R=f==="fallback"?i:e;x=this.findComponentChildrenById(A,R)}const L=rd.createRoot(_);this.skeletonOverlayRoot=L,ka.flushSync(()=>{L.render(Ye.createElement(c,{components:x,options:{animation:n.animation||"pulse",iteration_count:n.iteration_count||5}}))});const M=f==="fullpage"?"fullpage (#app)":f==="fallback"?`fallback (#${i})`:`target (#${e})`;U.log(`Skeleton overlay rendered [${M}] with "${n.component}" (${x.length} components)`)}findComponentChildrenById(e,n){const a=i=>{for(const l of i){if(l.id===n)return l.children||[];if(l.children&&Array.isArray(l.children)){const c=a(l.children);if(c!==null)return c}}return null};return a(e)||e}renderSpinnerOverlay(e,n,a){let i=document.getElementById(e),l="target";if(!i&&a&&(i=document.getElementById(a),l="fallback"),i||(i=document.getElementById("app"),l="fullpage"),!i){U.log(`Spinner overlay: no target found (target="#${e}", fallback="${a||"none"}"), falling back to opaque overlay`),this.
Line truncated
|
||||
`)(r)}return!!e}catch(a){return U.warn(`Failed to evaluate script condition: ${e}`,a),!1}}showInitError(e){if(ld()){U.warn("Init failed while document is unloading — skipping error screen",e);return}const r=cy(e);gf.renderFromError("app",r,"초기화 실패",this.config.debug,{templateId:this.config.templateId,locale:this.config.locale},void 0,e)}showTransitionOverlay(e,r){const a=typeof e=="boolean"?{enabled:e,style:"opaque",target:void 0,skeleton:void 0,spinner:void 0}:{enabled:e.enabled,style:e.style||"opaque",target:e.target,skeleton:e.skeleton,spinner:e.spinner,fallback_target:e.fallback_target};if(!a.enabled)return;if(this.hideTransitionOverlay(),a.style==="skeleton"&&a.skeleton?.component&&a.target&&r){this.renderSkeletonOverlay(a.target,a.skeleton,r,a.fallback_target);return}if(a.style==="spinner"&&a.target){this.renderSpinnerOverlay(a.target,a.spinner,a.fallback_target);return}const i=document.documentElement.classList.contains("dark");let l,c="";switch(a.style){case"blur":l=i?"rgba(17,24,39,0.3)":"rgba(255,255,255,0.3)",c="backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);";break;case"fade":l=i?"rgba(17,24,39,0.8)":"rgba(255,255,255,0.8)";break;case"skeleton":l=i?"rgb(17,24,39)":"rgb(249,250,251)";break;case"opaque":default:l=i?"rgb(17,24,39)":"rgb(249,250,251)";break}if(a.target){const d=`#${CSS.escape(a.target)}`,f=document.createElement("style");f.id="g7-transition-overlay",f.textContent=`${d}{position:relative;z-index:0;}${d}::after{content:'';position:absolute;inset:0;background:${l};${c}z-index:2147483647;pointer-events:none;}`,document.head.appendChild(f),this.transitionOverlayEl=f}else{const d=document.createElement("div");d.id="g7-transition-overlay",d.setAttribute("aria-hidden","true"),d.style.position="fixed",d.style.inset="0",d.style.zIndex="9999",d.style.pointerEvents="none",d.style.background=l;for(const f of c.split(";")){const g=f.indexOf(":");g!==-1&&d.style.setProperty(f.slice(0,g).trim(),f.slice(g+1).trim())}document.body.appendChild(d),this.transitionOverlayEl=d}}hideTransitionOverlay(){this.transitionOverlayEl&&(this.transitionOverlayEl.remove(),this.transitionOverlayEl=null),this.hideSkeletonOverlay()}renderSkeletonOverlay(e,r,a,i){const c=mr.getInstance().getComponent(r.component);if(!c){U.log(`Skeleton component "${r.component}" not found in registry, falling back to opaque overlay`),this.showTransitionOverlay({enabled:!0,style:"opaque",target:e});return}let d=document.getElementById(e),f="target";if(!d&&i&&(d=document.getElementById(i),f="fallback"),d||(d=document.getElementById("app"),f="fullpage"),!d){U.log(`Skeleton overlay: no target found (target="#${e}", fallback="${i||"none"}"), falling back to opaque overlay`),this.showTransitionOverlay({enabled:!0,style:"opaque",target:e});return}this.hideSkeletonOverlay();const m=document.documentElement.classList.contains("dark")?"rgb(17,24,39)":"rgb(249,250,251)",y=f==="fullpage"?"app":f==="fallback"?i:e,w=`#${CSS.escape(y)}`,S=document.createElement("style");S.id="g7-skeleton-overlay-style",S.textContent=`${w}{position:relative;z-index:0;}${w}::after{content:'';position:absolute;inset:0;background:${m};z-index:2147483646;pointer-events:none;}`,document.head.appendChild(S),this.transitionOverlayEl=S;const v=document.createElement("div");if(v.id="g7-skeleton-overlay",v.setAttribute("role","status"),v.setAttribute("aria-busy","true"),v.setAttribute("aria-label","Loading..."),f==="fullpage")v.style.cssText=["position:fixed","inset:0","z-index:20","overflow:hidden","pointer-events:none",`background:${m}`].join(";")+";";else{const O=d.getBoundingClientRect(),L=window.scrollX||document.documentElement.scrollLeft,q=window.scrollY||document.documentElement.scrollTop;v.style.cssText=["position:absolute",`top:${O.top+q}px`,`left:${O.left+L}px`,`width:${O.width}px`,`height:${O.height}px`,"z-index:20","overflow:hidden","pointer-events:none",`background:${m}`].join(";")+";"}document.body.appendChild(v),this.skeletonOverlayContainer=v;const A=a.components||[];let x;if(f==="fullpage")x=A;else{const O=f==="fallback"?i:e;x=this.findComponentChildrenById(A,O)}const R=rd.createRoot(v);this.skeletonOverlayRoot=R,Oa.flushSync(()=>{R.render(We.createElement(c,{components:x,options:{animation:r.animation||"pulse",iteration_count:r.iteration_count||5}}))});const M=f==="fullpage"?"fullpage (#app)":f==="fallback"?`fallback (#${i})`:`target (#${e})`;U.log(`Skeleton overlay rendered [${M}] with "${r.component}" (${x.length} components)`)}findComponentChildrenById(e,r){const a=i=>{for(const l of i){if(l.id===r)return l.children||[];if(l.children&&Array.isArray(l.children)){const c=a(l.children);if(c!==null)return c}}return null};return a(e)||e}renderSpinnerOverlay(e,r,a){let i=document.getElementById(e),l="target";if(!i&&a&&(i=document.getElementById(a),l="fallback"),i||(i=document.getElementById("app"),l="fullpage"),!i){U.log(`Spinner overlay: no target found (target="#${e}", fallback="${a||"none"}"), falling back to opaque ovLine truncated
|
||||
with(ctx) {
|
||||
try {
|
||||
return ${e};
|
||||
@@ -240,5 +283,5 @@ Error generating stack: `+u.message+`
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
`)(n)}catch(a){U.warn(`Expression evaluation failed: ${e}`,a);return}}async executeInitActions(e,n={}){const{getActionDispatcher:a}=await Promise.resolve().then(()=>Ur),i=a();if(!i){U.warn("ActionDispatcher not available for init_actions");return}U.log("Executing init_actions:",e.map(d=>d.handler)),U.log("Init actions dataContext:",n),U.log("Init actions dataContext._global:",n._global);const l=e.map(d=>d.handler).filter(d=>d.includes("."));l.length>0&&await this.waitForHandlers(i,l);let c={...n};for(const d of e)try{const f={type:"click",handler:d.handler,target:d.target,params:d.params,resultTo:d.resultTo,onSuccess:d.onSuccess,onError:d.onError,if:d.if,conditions:d.conditions,auth_mode:d.auth_mode,auth_required:d.auth_required},g=i.createHandler(f,c),m=new Event("init");await g(m),U.log(`Init action executed: ${d.handler}`);const b=this.globalState;c={...c,_global:{...b},_local:b._local||c._local||{}},U.log(`Data context refreshed after ${d.handler}:`,{cartKey:c._global?.cartKey})}catch(f){U.error(`Failed to execute init action: ${d.handler}`,f)}}async waitForHandlers(e,n,a=5e3){const i=Date.now(),l=50,c=()=>n.every(f=>e.customHandlers?.has(f));if(c()){U.log("All module handlers already registered");return}const d=wf().getFailedJsAssets();if(d.length>0){const f=d.some(b=>b==="module"||b==="plugin"),g=n.filter(b=>!e.customHandlers?.has(b)),m=g.length>0&&g.every(b=>d.some(S=>b.startsWith(`${S}.`)));if(f||m){U.warn("Extension asset load failed — not waiting for handlers that will never register:",{pending:g,failedAssets:d});return}}return U.log("Waiting for module handlers:",n),new Promise(f=>{const g=()=>{if(c()){U.log("All module handlers now registered"),f();return}if(Date.now()-i>=a){const m=n.filter(b=>!e.customHandlers?.has(b));U.warn("Timeout waiting for handlers:",m),f();return}setTimeout(g,l)};g()})}};$(Ar,"LOCALE_STORAGE_KEY","g7_locale"),$(Ar,"CACHE_VERSION_STORAGE_KEY","g7_cache_version");let xo=Ar;function Ef(o){const e=new xo(o);return o.websocket&&(vc.configure(o.websocket),U.log("WebSocket 설정 완료")),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>{e.init(),window.__templateApp=e}):(e.init(),window.__templateApp=e),e}typeof window<"u"&&(window.G7Core=window.G7Core||{},window.G7Core.initTemplateApp=Ef,window.G7Core.dataSource={refetch:async(o,e)=>{const n=window.__templateApp;if(!n){U.warn("TemplateApp not initialized (G7Core.dataSource.refetch)");return}return n.refetchDataSource(o,e)},get:o=>{const e=window.__templateApp;if(!e){U.warn("TemplateApp not initialized (G7Core.dataSource.get)");return}return e.getDataSource(o)}});function rA(o={}){const{checked:e,value:n,name:a,type:i="checkbox"}=o,c=n!==void 0?n:i==="checkbox"||i==="radio"?e??!1:String(e??""),d={checked:e??!1,value:c,name:a??"",type:i};return{target:d,currentTarget:d,preventDefault:()=>{},stopPropagation:()=>{},nativeEvent:new Event("change"),bubbles:!0,cancelable:!0,defaultPrevented:!1,eventPhase:Event.AT_TARGET,isTrusted:!1,timeStamp:Date.now(),type:"change",isDefaultPrevented:()=>!1,isPropagationStopped:()=>!1,persist:()=>{}}}function aA(o={}){const{button:e=0,clientX:n=0,clientY:a=0}=o;return{button:e,clientX:n,clientY:a,preventDefault:()=>{},stopPropagation:()=>{},nativeEvent:new MouseEvent("click"),bubbles:!0,cancelable:!0,defaultPrevented:!1,eventPhase:Event.AT_TARGET,isTrusted:!1,timeStamp:Date.now(),type:"click",isDefaultPrevented:()=>!1,isPropagationStopped:()=>!1,persist:()=>{},target:document.createElement("div"),currentTarget:document.createElement("div")}}function iA(){return{preventDefault:()=>{},stopPropagation:()=>{},nativeEvent:new Event("submit"),bubbles:!0,cancelable:!0,defaultPrevented:!1,eventPhase:Event.AT_TARGET,isTrusted:!1,timeStamp:Date.now(),type:"submit",isDefaultPrevented:()=>!1,isPropagationStopped:()=>!1,persist:()=>{},target:document.createElement("form"),currentTarget:document.createElement("form")}}function sA(o,e="keydown"){return{key:o,code:o.length===1?`Key${o.toUpperCase()}`:o,preventDefault:()=>{},stopPropagation:()=>{},nativeEvent:new KeyboardEvent(e,{key:o}),bubbles:!0,cancelable:!0,defaultPrevented:!1,eventPhase:Event.AT_TARGET,isTrusted:!1,timeStamp:Date.now(),type:e,isDefaultPrevented:()=>!1,isPropagationStopped:()=>!1,persist:()=>{},target:document.createElement("div"),currentTarget:document.createElement("div"),altKey:!1,ctrlKey:!1,metaKey:!1,shiftKey:!1,repeat:!1}}const oA={justify:/^justify-(start|end|center|between|around|evenly)$/,items:/^items-(start|end|center|baseline|stretch)$/,content:/^content-(start|end|center|between|around|evenly|stretch)$/,self:/^self-(auto|start|end|center|stretch|baseline)$/,flex:/^flex-(row|row-reverse|col|col-reverse|wrap|wrap-reverse|nowrap|1|auto|initial|none)$/,grow:/^(grow|grow-0)$/,shrink:/^(shrink|shrink-0)$/,basis:/^basis-/,order:/^(order-|-)order-/,gap:/^gap(-x|-y)?-/,gridCols:/^grid-cols-/,gridRows:/^grid-rows-/,colSpan:/^col-(span-|start-|end-)/,rowSpan:/^row-(span-|start-|end-)/,displa
Line truncated
|
||||
`)(r)}catch(a){U.warn(`Expression evaluation failed: ${e}`,a);return}}async executeInitActions(e,r={}){const{getActionDispatcher:a}=await Promise.resolve().then(()=>Pr),i=a();if(!i){U.warn("ActionDispatcher not available for init_actions");return}U.log("Executing init_actions:",e.map(d=>d.handler)),U.log("Init actions dataContext:",r),U.log("Init actions dataContext._global:",r._global);const l=e.map(d=>d.handler).filter(d=>d.includes("."));l.length>0&&await this.waitForHandlers(i,l);let c={...r};for(const d of e)try{const f={type:"click",handler:d.handler,target:d.target,params:d.params,resultTo:d.resultTo,onSuccess:d.onSuccess,onError:d.onError,if:d.if,conditions:d.conditions,auth_mode:d.auth_mode,auth_required:d.auth_required},g=i.createHandler(f,c),m=new Event("init");await g(m),U.log(`Init action executed: ${d.handler}`);const y=this.globalState;c={...c,_global:{...y},_local:y._local||c._local||{}},U.log(`Data context refreshed after ${d.handler}:`,{cartKey:c._global?.cartKey})}catch(f){U.error(`Failed to execute init action: ${d.handler}`,f)}}async waitForHandlers(e,r,a=5e3){const i=Date.now(),l=50,c=()=>r.every(f=>e.customHandlers?.has(f));if(c()){U.log("All module handlers already registered");return}const d=mf().getFailedJsAssets();if(d.length>0){const f=d.some(y=>y==="module"||y==="plugin"),g=r.filter(y=>!e.customHandlers?.has(y)),m=g.length>0&&g.every(y=>d.some(w=>y.startsWith(`${w}.`)));if(f||m){U.warn("Extension asset load failed — not waiting for handlers that will never register:",{pending:g,failedAssets:d});return}}return U.log("Waiting for module handlers:",r),new Promise(f=>{const g=()=>{if(c()){U.log("All module handlers now registered"),f();return}if(Date.now()-i>=a){const m=r.filter(y=>!e.customHandlers?.has(y));U.warn("Timeout waiting for handlers:",m),f();return}setTimeout(g,l)};g()})}};$(xr,"LOCALE_STORAGE_KEY","g7_locale"),$(xr,"CACHE_VERSION_STORAGE_KEY","g7_cache_version");let To=xr;function bf(o){const e=new To(o);return o.websocket&&(Tc.configure(o.websocket),U.log("WebSocket 설정 완료")),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>{e.init(),window.__templateApp=e}):(e.init(),window.__templateApp=e),e}typeof window<"u"&&(window.G7Core=window.G7Core||{},window.G7Core.initTemplateApp=bf,window.G7Core.dataSource={refetch:async(o,e)=>{const r=window.__templateApp;if(!r){U.warn("TemplateApp not initialized (G7Core.dataSource.refetch)");return}return r.refetchDataSource(o,e)},get:o=>{const e=window.__templateApp;if(!e){U.warn("TemplateApp not initialized (G7Core.dataSource.get)");return}return e.getDataSource(o)}});function N_(o={}){const{checked:e,value:r,name:a,type:i="checkbox"}=o,c=r!==void 0?r:i==="checkbox"||i==="radio"?e??!1:String(e??""),d={checked:e??!1,value:c,name:a??"",type:i};return{target:d,currentTarget:d,preventDefault:()=>{},stopPropagation:()=>{},nativeEvent:new Event("change"),bubbles:!0,cancelable:!0,defaultPrevented:!1,eventPhase:Event.AT_TARGET,isTrusted:!1,timeStamp:Date.now(),type:"change",isDefaultPrevented:()=>!1,isPropagationStopped:()=>!1,persist:()=>{}}}function I_(o={}){const{button:e=0,clientX:r=0,clientY:a=0}=o;return{button:e,clientX:r,clientY:a,preventDefault:()=>{},stopPropagation:()=>{},nativeEvent:new MouseEvent("click"),bubbles:!0,cancelable:!0,defaultPrevented:!1,eventPhase:Event.AT_TARGET,isTrusted:!1,timeStamp:Date.now(),type:"click",isDefaultPrevented:()=>!1,isPropagationStopped:()=>!1,persist:()=>{},target:document.createElement("div"),currentTarget:document.createElement("div")}}function H_(){return{preventDefault:()=>{},stopPropagation:()=>{},nativeEvent:new Event("submit"),bubbles:!0,cancelable:!0,defaultPrevented:!1,eventPhase:Event.AT_TARGET,isTrusted:!1,timeStamp:Date.now(),type:"submit",isDefaultPrevented:()=>!1,isPropagationStopped:()=>!1,persist:()=>{},target:document.createElement("form"),currentTarget:document.createElement("form")}}function j_(o,e="keydown"){return{key:o,code:o.length===1?`Key${o.toUpperCase()}`:o,preventDefault:()=>{},stopPropagation:()=>{},nativeEvent:new KeyboardEvent(e,{key:o}),bubbles:!0,cancelable:!0,defaultPrevented:!1,eventPhase:Event.AT_TARGET,isTrusted:!1,timeStamp:Date.now(),type:e,isDefaultPrevented:()=>!1,isPropagationStopped:()=>!1,persist:()=>{},target:document.createElement("div"),currentTarget:document.createElement("div"),altKey:!1,ctrlKey:!1,metaKey:!1,shiftKey:!1,repeat:!1}}const z_={justify:/^justify-(start|end|center|between|around|evenly)$/,items:/^items-(start|end|center|baseline|stretch)$/,content:/^content-(start|end|center|between|around|evenly|stretch)$/,self:/^self-(auto|start|end|center|stretch|baseline)$/,flex:/^flex-(row|row-reverse|col|col-reverse|wrap|wrap-reverse|nowrap|1|auto|initial|none)$/,grow:/^(grow|grow-0)$/,shrink:/^(shrink|shrink-0)$/,basis:/^basis-/,order:/^(order-|-)order-/,gap:/^gap(-x|-y)?-/,gridCols:/^grid-cols-/,gridRows:/^grid-rows-/,colSpan:/^col-(span-|start-|end-)/,rowSpan:/^row-(span-|start-|end-)/,displa
Line truncated
|
||||
//# sourceMappingURL=template-engine.min.js.map
|
||||
@@ -4222,6 +4222,41 @@ export class ActionDispatcher {
|
||||
const update = this.createNestedUpdate(path, value, currentState);
|
||||
const mergedState = { ...currentState, ...update };
|
||||
context.setState(update);
|
||||
|
||||
// engine-v1.58.2: dot notation 경로도 canonical source(_global._local)에 동기화한다.
|
||||
//
|
||||
// 배경: 이 분기는 context.setState(저장소 A: React localDynamicState)만 갱신하고
|
||||
// _global._local(저장소 B)은 갱신하지 않았다. 커스텀 핸들러(모듈/플러그인)가 상태를
|
||||
// 읽는 유일한 공개 통로인 G7Core.state.getLocal() 은 B 를 읽으므로, dot notation 으로
|
||||
// 기록된 값은 핸들러에게 undefined 로 보였다 (체크아웃 간편결제 선택 → PG 플러그인이
|
||||
// 선택값을 못 읽어 통합결제창이 열린 결함). target: "local" 형태는 engine-v1.50.0 에서
|
||||
// 이미 B 를 동기화하므로, 남아 있던 비대칭을 해소한다.
|
||||
//
|
||||
// [동기화 기준을 전체 스냅샷이 아니라 "live B + 변경 키"로 잡은 이유]
|
||||
// COMPONENT path(target:"local")는 currentState(=pending ?? context.state) 전체 스냅샷을
|
||||
// B 에 통째로 넘긴다. context.state 는 클릭된 리프 컴포넌트의 부분 상태일 수 있어
|
||||
// B 의 다른 키를 잃을 수 있고(engine-v1.50.0 주석의 안전성 의존 관계), 앞선
|
||||
// 커스텀 핸들러 setLocal 결과를 stale base 로 되돌릴 수 있다(트러블슈팅 사례 24).
|
||||
// 여기서는 live B 를 base 로 삼고 변경된 최상위 키만 얹으므로 두 위험이 모두 없다.
|
||||
// B 를 못 읽는 환경(테스트 등 __templateApp 부재)에서만 전체 스냅샷으로 폴백한다.
|
||||
//
|
||||
// [__g7PendingLocalState 를 쓰지 않는 이유]
|
||||
// pending 에 B 기반 전체 스냅샷을 넣으면 handleLocalSetState 의 effectivePrev 병합에서
|
||||
// React 전용 상태(DataGrid expandedRows 등)를 초기값으로 덮어쓴다(트러블슈팅 사례 22).
|
||||
// setGlobalState 는 globalState 를 동기 대입하므로 pending 없이도 같은 tick 의
|
||||
// getLocal() 이 즉시 최신값을 읽는다 — 오염 경로에 진입할 이유가 없다.
|
||||
//
|
||||
// scope: 'parent' | 'root' 은 이 분기가 구현하지 않는 타깃이므로 동기화 대상에서 제외한다
|
||||
// (모달에서 부모 스코프를 노린 setState 가 페이지 저장소를 오염시키는 것 방지 — 사례 29).
|
||||
if (this.globalStateUpdater && scope !== 'parent' && scope !== 'root') {
|
||||
const canonicalLocal = (window as any).__templateApp?.getGlobalState?.()?._local;
|
||||
const syncedLocal = canonicalLocal && typeof canonicalLocal === 'object'
|
||||
? { ...canonicalLocal, ...update }
|
||||
: mergedState;
|
||||
this.globalStateUpdater({ _local: syncedLocal }, { render: false });
|
||||
logger.log('[handleSetState] _global._local synced for dot notation (render:false):', syncedLocal);
|
||||
}
|
||||
|
||||
if (setStateId && devTools) setTimeout(() => devTools.completeStateChange(setStateId), 0);
|
||||
return mergedState;
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,20 @@
|
||||
>
|
||||
> 형식: [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)
|
||||
|
||||
## [engine-v1.58.2] - 2026-08-10
|
||||
|
||||
### Fixed
|
||||
|
||||
#### `target: "_local.xxx"` 로 기록한 값을 커스텀 핸들러가 읽지 못하던 문제
|
||||
|
||||
- `ActionDispatcher.ts::handleSetState` — dot notation 경로(`target: "_local.paymentMethod"`)로 기록한 값을 canonical source 인 `_global._local` 에도 동기화한다. 기존에는 `context.setState`(React `localDynamicState`)만 갱신하고 `_global._local` 은 갱신하지 않았다. 모듈·플러그인 커스텀 핸들러가 상태를 읽는 공개 통로인 `G7Core.state.getLocal()` 은 `_global._local` 을 읽으므로, dot notation 으로 기록된 값은 핸들러에게 `undefined` 로 보였다.
|
||||
- 화면은 정상으로 보인다 — 선택 표시는 React 저장소만으로 그려지기 때문이다. **예외도 경고도 콘솔 출력도 없이 핸들러가 받는 값만 비어** 발견이 늦다. 실제 발현: 주문서형 결제에서 간편결제(네이버페이 등)를 고르고 결제하면 PG 플러그인이 선택값을 못 읽어 간편결제 자체창 대신 통합결제창이 열렸다.
|
||||
- `target: "local"` 형태는 engine-v1.50.0 에서 이미 `_global._local` 을 동기화하고 있었다. 같은 뜻의 두 표기가 서로 다른 저장소에 쓰이던 비대칭을 해소한 것이다.
|
||||
- 동기화 기준은 **현재 `_global._local` + 변경된 최상위 키**다. `target: "local"` 경로처럼 전체 스냅샷을 넘기지 않는다 — 클릭된 리프 컴포넌트의 부분 상태가 base 가 되면 `_global._local` 의 다른 키를 잃거나, 앞서 커스텀 핸들러가 `setLocal()` 로 기록한 값을 되돌릴 수 있다.
|
||||
- `__g7PendingLocalState` 는 갱신하지 않는다. `_global._local` 기반 전체 스냅샷을 pending 에 넣으면 DataGrid `expandedRows` 처럼 React 에만 존재하는 상태를 초기값으로 덮어쓴다. 전역 상태 대입은 동기적이라 pending 없이도 같은 tick 의 `getLocal()` 이 최신값을 읽는다.
|
||||
- `scope: "parent" | "root"` 는 dot notation 분기가 구현하지 않는 타깃이므로 동기화 대상에서 제외한다(모달에서 부모 스코프를 노린 `setState` 가 페이지 저장소를 오염시키는 것 방지).
|
||||
- 저장소 영향 범위(전수 확인): `target: "_local.xxx"` 사용은 8건이며 전부 `sirsoft-basic` 체크아웃 3개 파일이다(결제수단 선택·무통장 은행 선택·환불계좌 초기화·주문 제출 플래그). TS/TSX 코드에서 이 형태로 dispatch 하는 곳과 `$parent._local.xxx` / `$root._local.xxx` 사용은 0건이다.
|
||||
|
||||
## [engine-v1.58.1] - 2026-08-07
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -5151,3 +5151,121 @@ describe('[사례 38] 액션을 event 로 적은 경우의 prop 이름과 $event
|
||||
expect(captured?.form?.content).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* [사례 39] setState `target: "_local.xxx"` 가 canonical source(_global._local)에 반영되지 않음
|
||||
*
|
||||
* 배경: 체크아웃에서 간편결제(네이버페이)를 고르고 결제하면 자체창이 아니라 통합결제창이 열렸다.
|
||||
* 결제수단 선택은 `{"target": "_local.paymentMethod"}` 로 기록되는데, dot notation 분기는
|
||||
* `context.setState`(저장소 A: React localDynamicState)만 갱신하고 `_global._local`(저장소 B)은
|
||||
* 갱신하지 않았다. PG 플러그인 핸들러는 `G7Core.state.getLocal()`(저장소 B 를 읽음)로 선택값을
|
||||
* 조회하므로 undefined 를 받았고, 간편결제 지정이 통째로 누락된 채 통합결제창이 열렸다.
|
||||
*
|
||||
* `target: "local"` 형태는 engine-v1.50.0 에서 이미 B 를 동기화하고 있었다 — dot notation 만
|
||||
* 남겨진 비대칭이었다.
|
||||
*
|
||||
* @see 트러블슈팅 가이드 "setState & init_actions & dataKey" 사례 39
|
||||
*/
|
||||
describe('[사례 39] dot notation setState 의 canonical source 동기화 (engine-v1.58.2)', () => {
|
||||
let dispatcher: ActionDispatcher;
|
||||
let globalState: Record<string, any>;
|
||||
let componentState: Record<string, any>;
|
||||
let setStateCalls: any[];
|
||||
|
||||
/** TemplateApp.setGlobalState 와 동일한 계약: 최상위 키 얕은 병합 + 동기 대입 */
|
||||
const globalStateUpdater = vi.fn((updates: any) => {
|
||||
globalState = { ...globalState, ...updates };
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setStateCalls = [];
|
||||
componentState = {};
|
||||
globalState = { _local: {} };
|
||||
|
||||
dispatcher = new ActionDispatcher({ navigate: vi.fn() });
|
||||
dispatcher.setGlobalStateUpdater(globalStateUpdater);
|
||||
Logger.getInstance().setDebug(false);
|
||||
|
||||
(window as any).__templateApp = { getGlobalState: () => globalState };
|
||||
(window as any).__g7PendingLocalState = undefined;
|
||||
(window as any).__g7ForcedLocalFields = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
delete (window as any).__templateApp;
|
||||
(window as any).__g7PendingLocalState = undefined;
|
||||
(window as any).__g7ForcedLocalFields = undefined;
|
||||
});
|
||||
|
||||
const context = () => ({
|
||||
state: componentState,
|
||||
setState: vi.fn((updates: any) => {
|
||||
setStateCalls.push(updates);
|
||||
componentState = { ...componentState, ...updates };
|
||||
}),
|
||||
actionId: 'case-39',
|
||||
});
|
||||
|
||||
it('결함 재현: 선택값이 _global._local 에 반영되어 getLocal() 경로로 읽힌다', async () => {
|
||||
await dispatcher.executeAction(
|
||||
{ handler: 'setState' as const, params: { target: '_local.paymentMethod', value: 'toss_naverpay' } },
|
||||
context()
|
||||
);
|
||||
|
||||
expect(globalState._local.paymentMethod).toBe('toss_naverpay');
|
||||
});
|
||||
|
||||
it('사례 13·24 핀: canonical source 의 기존 키를 하나도 잃지 않는다 (leaf partial context 에서도)', async () => {
|
||||
// 커스텀 핸들러의 setLocal 등으로 이미 canonical store 에 쌓여 있는 값
|
||||
globalState._local = { orderer: { name: '관리자' }, shipping: { zipcode: '94271' }, couponId: 7 };
|
||||
// 클릭된 리프 컴포넌트의 context.state 는 페이지 _local 전체가 아니다
|
||||
componentState = { paymentMethod: 'toss_card' };
|
||||
|
||||
await dispatcher.executeAction(
|
||||
{ handler: 'setState' as const, params: { target: '_local.paymentMethod', value: 'toss_naverpay' } },
|
||||
context()
|
||||
);
|
||||
|
||||
expect(globalState._local.paymentMethod).toBe('toss_naverpay');
|
||||
expect(globalState._local.orderer).toEqual({ name: '관리자' });
|
||||
expect(globalState._local.shipping).toEqual({ zipcode: '94271' });
|
||||
expect(globalState._local.couponId).toBe(7);
|
||||
});
|
||||
|
||||
it('사례 22 핀: __g7PendingLocalState 를 건드리지 않는다 (React 전용 배열 오염 경로 차단)', async () => {
|
||||
(window as any).__g7PendingLocalState = { expandedRows: [301] };
|
||||
|
||||
await dispatcher.executeAction(
|
||||
{ handler: 'setState' as const, params: { target: '_local.paymentMethod', value: 'toss_naverpay' } },
|
||||
context()
|
||||
);
|
||||
|
||||
expect((window as any).__g7PendingLocalState).toEqual({ expandedRows: [301] });
|
||||
});
|
||||
|
||||
it('사례 29 핀: scope parent/root 는 동기화 대상이 아니다 (모달 컨텍스트 오염 방지)', async () => {
|
||||
await dispatcher.executeAction(
|
||||
{
|
||||
handler: 'setState' as const,
|
||||
params: { target: '_local.paymentMethod', scope: 'parent', value: 'toss_naverpay' },
|
||||
},
|
||||
context()
|
||||
);
|
||||
|
||||
expect(globalState._local.paymentMethod).toBeUndefined();
|
||||
});
|
||||
|
||||
it('기존 동작 유지: context.setState 에는 여전히 중첩 경로 변경분만 전달된다', async () => {
|
||||
componentState = { form: { title: '기존', body: '유지' } };
|
||||
|
||||
await dispatcher.executeAction(
|
||||
{ handler: 'setState' as const, params: { target: '_local.form.title', value: '변경' } },
|
||||
context()
|
||||
);
|
||||
|
||||
expect(setStateCalls).toHaveLength(1);
|
||||
expect(setStateCalls[0]).toEqual({ form: { title: '변경', body: '유지' } });
|
||||
expect(globalState._local.form).toEqual({ title: '변경', body: '유지' });
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,59 @@ function scanExtensions($path) {
|
||||
return array_values($extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 디렉토리 하위 항목을 삭제합니다 (디렉토리 자체는 보존).
|
||||
*
|
||||
* 인스톨러 초기화에서 "설정/업로드 내용물만 비우고 디렉토리 구조는 남긴다" 는
|
||||
* 용도로 사용합니다. 디렉토리 자체를 지우면 권한/소유권이 재생성 시점의 실행
|
||||
* 계정으로 바뀌므로 컨테이너를 보존합니다.
|
||||
*
|
||||
* @param string $dir 대상 디렉토리 절대경로
|
||||
* @param array $keep 보존할 항목명 목록 (basename 기준, 예: ['settings', '.preserve-ownership'])
|
||||
* @return array{deleted: int, failed: int, names: array} 삭제 건수 / 실패 건수 / 삭제된 항목명
|
||||
*/
|
||||
function devDashboardClearDirectory(string $dir, array $keep = []): array
|
||||
{
|
||||
$result = ['deleted' => 0, 'failed' => 0, 'names' => []];
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
|
||||
$keep = array_merge(['.', '..', '.gitignore', '.gitkeep'], $keep);
|
||||
|
||||
foreach (scandir($dir) as $item) {
|
||||
if (in_array($item, $keep, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $dir . DIRECTORY_SEPARATOR . $item;
|
||||
|
||||
if (is_dir($path)) {
|
||||
$escaped = escapeshellarg($path);
|
||||
if ($isWindows) {
|
||||
exec("rmdir /s /q $escaped 2>nul");
|
||||
} else {
|
||||
exec("rm -rf $escaped");
|
||||
}
|
||||
if (is_dir($path)) {
|
||||
$result['failed']++;
|
||||
} else {
|
||||
$result['deleted']++;
|
||||
$result['names'][] = $item;
|
||||
}
|
||||
} elseif (@unlink($path)) {
|
||||
$result['deleted']++;
|
||||
$result['names'][] = $item;
|
||||
} else {
|
||||
$result['failed']++;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$modules = scanExtensions(base_path('modules'));
|
||||
$templates = scanExtensions(base_path('templates'));
|
||||
$plugins = scanExtensions(base_path('plugins'));
|
||||
@@ -366,6 +419,110 @@ if (isset($_GET['ajax_action'])) {
|
||||
}
|
||||
}
|
||||
|
||||
// 환경설정 파일 삭제 (코어 + 모듈 + 플러그인 + 템플릿)
|
||||
$deleteSettings = isset($_GET['delete_settings']) && $_GET['delete_settings'] === '1';
|
||||
if ($deleteSettings) {
|
||||
// 코어: storage/app/settings/*.json (SettingsService 의 SSoT)
|
||||
$coreSettingsPath = storage_path('app/settings');
|
||||
$cleared = devDashboardClearDirectory($coreSettingsPath);
|
||||
if ($cleared['deleted'] === 0 && $cleared['failed'] === 0) {
|
||||
$results[] = ['name' => '코어 환경설정 (storage/app/settings)', 'status' => 'notfound'];
|
||||
} else {
|
||||
$results[] = [
|
||||
'name' => "코어 환경설정 (storage/app/settings, {$cleared['deleted']}개)",
|
||||
'status' => $cleared['failed'] === 0 ? 'deleted' : 'failed'
|
||||
];
|
||||
}
|
||||
|
||||
// 모듈/플러그인: storage/app/{modules,plugins}/{identifier}/settings/
|
||||
// (ModuleSettingsService / PluginSettingsService 가 StorageInterface 로 기록하는 경로)
|
||||
foreach (['modules' => '모듈', 'plugins' => '플러그인'] as $storageDir => $label) {
|
||||
$basePath = storage_path('app/' . $storageDir);
|
||||
$clearedIds = [];
|
||||
$failedIds = [];
|
||||
if (is_dir($basePath)) {
|
||||
foreach (array_filter(glob($basePath . '/*'), 'is_dir') as $extDir) {
|
||||
$settingsDir = $extDir . DIRECTORY_SEPARATOR . 'settings';
|
||||
if (!is_dir($settingsDir)) {
|
||||
continue;
|
||||
}
|
||||
$cleared = devDashboardClearDirectory($settingsDir);
|
||||
if ($cleared['failed'] > 0) {
|
||||
$failedIds[] = basename($extDir);
|
||||
} elseif ($cleared['deleted'] > 0) {
|
||||
$clearedIds[] = basename($extDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (empty($clearedIds) && empty($failedIds)) {
|
||||
$results[] = ['name' => "{$label} 환경설정 (storage/app/{$storageDir})", 'status' => 'notfound'];
|
||||
} else {
|
||||
$detail = implode(', ', array_merge($clearedIds, $failedIds));
|
||||
$results[] = [
|
||||
'name' => "{$label} 환경설정 ({$detail})",
|
||||
'status' => empty($failedIds) ? 'deleted' : 'failed'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 템플릿은 파일 기반 환경설정 저장소를 갖지 않는다.
|
||||
// 활성 상태/레이아웃 편집분은 DB(templates, template_layouts), 나머지는 템플릿 패키지 파일이므로
|
||||
// 각각 "DB 테이블 전체 삭제" 와 "확장 설치 디렉토리 삭제" 가 담당한다.
|
||||
// 삭제 대상이 0 건인 것과 대상 자체가 없는 것은 다르므로 그 사실을 그대로 표기한다.
|
||||
$results[] = [
|
||||
'name' => '템플릿 환경설정 (파일 저장소 없음 — DB/패키지 보관)',
|
||||
'status' => 'notfound'
|
||||
];
|
||||
}
|
||||
|
||||
// 첨부파일 스토리지 삭제
|
||||
$deleteAttachments = isset($_GET['delete_attachments']) && $_GET['delete_attachments'] === '1';
|
||||
if ($deleteAttachments) {
|
||||
// 코어 첨부파일 디스크(config/attachment.php) + 공개 업로드 디스크
|
||||
$attachmentPaths = [
|
||||
'storage/app/attachments' => storage_path('app/attachments'),
|
||||
'storage/app/public' => storage_path('app/public')
|
||||
];
|
||||
foreach ($attachmentPaths as $displayName => $path) {
|
||||
$cleared = devDashboardClearDirectory($path);
|
||||
if ($cleared['deleted'] === 0 && $cleared['failed'] === 0) {
|
||||
$results[] = ['name' => $displayName, 'status' => 'notfound'];
|
||||
} else {
|
||||
$results[] = [
|
||||
'name' => "{$displayName} ({$cleared['deleted']}개)",
|
||||
'status' => $cleared['failed'] === 0 ? 'deleted' : 'failed'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 모듈/플러그인 업로드 파일 (예: storage/app/modules/{id}/images/products)
|
||||
// settings/ 와 .preserve-ownership 마커는 보존 — 환경설정 삭제 옵션과 서로 간섭하지 않도록 분리한다.
|
||||
foreach (['modules' => '모듈', 'plugins' => '플러그인'] as $storageDir => $label) {
|
||||
$basePath = storage_path('app/' . $storageDir);
|
||||
$clearedIds = [];
|
||||
$failedIds = [];
|
||||
if (is_dir($basePath)) {
|
||||
foreach (array_filter(glob($basePath . '/*'), 'is_dir') as $extDir) {
|
||||
$cleared = devDashboardClearDirectory($extDir, ['settings', '.preserve-ownership']);
|
||||
if ($cleared['failed'] > 0) {
|
||||
$failedIds[] = basename($extDir);
|
||||
} elseif ($cleared['deleted'] > 0) {
|
||||
$clearedIds[] = basename($extDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (empty($clearedIds) && empty($failedIds)) {
|
||||
$results[] = ['name' => "{$label} 업로드 파일 (storage/app/{$storageDir})", 'status' => 'notfound'];
|
||||
} else {
|
||||
$detail = implode(', ', array_merge($clearedIds, $failedIds));
|
||||
$results[] = [
|
||||
'name' => "{$label} 업로드 파일 ({$detail})",
|
||||
'status' => empty($failedIds) ? 'deleted' : 'failed'
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'results' => $results]);
|
||||
exit;
|
||||
}
|
||||
@@ -1002,6 +1159,24 @@ if (isset($_GET['ajax_action'])) {
|
||||
<div class="absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-4"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-slate-400 bg-slate-900/30 px-3 py-2 rounded-lg">
|
||||
<span>환경설정 파일 삭제</span>
|
||||
<span class="text-[10px] text-slate-500">(코어, 모듈, 플러그인)</span>
|
||||
<label class="relative cursor-pointer ml-auto">
|
||||
<input type="checkbox" id="deleteSettings" class="sr-only peer">
|
||||
<div class="w-9 h-5 bg-slate-700 rounded-full peer peer-checked:bg-red-600 transition-colors"></div>
|
||||
<div class="absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-4"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-slate-400 bg-slate-900/30 px-3 py-2 rounded-lg">
|
||||
<span>첨부파일 스토리지 삭제</span>
|
||||
<span class="text-[10px] text-slate-500">(업로드 파일 전체)</span>
|
||||
<label class="relative cursor-pointer ml-auto">
|
||||
<input type="checkbox" id="deleteAttachments" class="sr-only peer">
|
||||
<div class="w-9 h-5 bg-slate-700 rounded-full peer peer-checked:bg-red-600 transition-colors"></div>
|
||||
<div class="absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-4"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1239,6 +1414,8 @@ if (isset($_GET['ajax_action'])) {
|
||||
const deleteVendor = document.getElementById('deleteVendor').checked;
|
||||
const deleteExtensions = document.getElementById('deleteExtensions').checked;
|
||||
const dropTables = document.getElementById('dropTables').checked;
|
||||
const deleteSettings = document.getElementById('deleteSettings').checked;
|
||||
const deleteAttachments = document.getElementById('deleteAttachments').checked;
|
||||
let message = '⚠️ 인스톨러 초기화\n\n';
|
||||
message += '다음 항목이 삭제됩니다:\n';
|
||||
message += ' • .env\n';
|
||||
@@ -1255,6 +1432,17 @@ if (isset($_GET['ajax_action'])) {
|
||||
if (dropTables) {
|
||||
message += ' • DB 테이블 전체 삭제 (migrations 포함)\n';
|
||||
}
|
||||
if (deleteSettings) {
|
||||
message += ' • storage/app/settings/ (코어 환경설정)\n';
|
||||
message += ' • storage/app/modules/*/settings/ (모듈 환경설정)\n';
|
||||
message += ' • storage/app/plugins/*/settings/ (플러그인 환경설정)\n';
|
||||
message += ' ※ 템플릿은 파일 환경설정이 없음 (DB/패키지 보관)\n';
|
||||
}
|
||||
if (deleteAttachments) {
|
||||
message += ' • storage/app/attachments/ (첨부파일)\n';
|
||||
message += ' • storage/app/public/ (공개 업로드 파일)\n';
|
||||
message += ' • storage/app/modules|plugins/*/ 업로드 파일 (settings 보존)\n';
|
||||
}
|
||||
message += '\n계속하시겠습니까?';
|
||||
|
||||
if (!confirm(message)) return;
|
||||
@@ -1266,7 +1454,7 @@ if (isset($_GET['ajax_action'])) {
|
||||
panel.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const url = `${window.location.pathname}?ajax_action=reset&delete_vendor=${deleteVendor ? '1' : '0'}&delete_extensions=${deleteExtensions ? '1' : '0'}&drop_tables=${dropTables ? '1' : '0'}`;
|
||||
const url = `${window.location.pathname}?ajax_action=reset&delete_vendor=${deleteVendor ? '1' : '0'}&delete_extensions=${deleteExtensions ? '1' : '0'}&drop_tables=${dropTables ? '1' : '0'}&delete_settings=${deleteSettings ? '1' : '0'}&delete_attachments=${deleteAttachments ? '1' : '0'}`;
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.4] - 2026-07-17
|
||||
## [1.0.4] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.1.0] - 2026-08-06
|
||||
## [1.1.0] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user